diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ab9704c0af0ff..27b468612fa6c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,7 +9,10 @@ on:
# See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push.
push:
branches:
- - '**'
+ # Disable CI on branch pushes to forks. It will still run for pull requests.
+ # This prevents CI from running twice for typical pull request workflows.
+ - 'bitcoin/**'
+ - 'bitcoin-core/**'
tags-ignore:
- '**'
diff --git a/contrib/completions/bash/bitcoin-cli.bash b/contrib/completions/bash/bitcoin-cli.bash
index 89e01bc09ae69..b04fdbcb0e870 100644
--- a/contrib/completions/bash/bitcoin-cli.bash
+++ b/contrib/completions/bash/bitcoin-cli.bash
@@ -9,7 +9,7 @@ _bitcoin_rpc() {
local rpcargs=()
for i in ${COMP_LINE}; do
case "$i" in
- -conf=*|-datadir=*|-regtest|-rpc*|-testnet)
+ -conf=*|-datadir=*|-regtest|-rpc*|-testnet|-testnet4)
rpcargs=( "${rpcargs[@]}" "$i" )
;;
esac
diff --git a/contrib/signet/README.md b/contrib/signet/README.md
index 706b296c54942..3303740f579fd 100644
--- a/contrib/signet/README.md
+++ b/contrib/signet/README.md
@@ -44,6 +44,8 @@ Adding the --ongoing parameter will then cause the signet miner to create blocks
$MINER --cli="$CLI" generate --grind-cmd="$GRIND" --address="$ADDR" --nbits=$NBITS --ongoing
+For custom signets with a trivial challenge a PSBT is not necessary. The miner detects this for `OP_TRUE`.
+
Other options
-------------
@@ -80,4 +82,3 @@ These steps can instead be done explicitly:
$CLI -signet -stdin submitblock
This is intended to allow you to replace part of the pipeline for further experimentation (eg, to sign the block with a hardware wallet).
-
diff --git a/contrib/signet/miner b/contrib/signet/miner
index 4216ada5fa633..f6787ca95acce 100755
--- a/contrib/signet/miner
+++ b/contrib/signet/miner
@@ -96,9 +96,10 @@ def do_decode_psbt(b64psbt):
return from_binary(CBlock, psbt.g.map[PSBT_SIGNET_BLOCK]), ser_string(scriptSig) + scriptWitness
def finish_block(block, signet_solution, grind_cmd):
- block.vtx[0].vout[-1].scriptPubKey += CScriptOp.encode_op_pushdata(SIGNET_HEADER + signet_solution)
- block.vtx[0].rehash()
- block.hashMerkleRoot = block.calc_merkle_root()
+ if signet_solution is not None:
+ block.vtx[0].vout[-1].scriptPubKey += CScriptOp.encode_op_pushdata(SIGNET_HEADER + signet_solution)
+ block.vtx[0].rehash()
+ block.hashMerkleRoot = block.calc_merkle_root()
if grind_cmd is None:
block.solve()
else:
@@ -110,10 +111,12 @@ def finish_block(block, signet_solution, grind_cmd):
block.rehash()
return block
-def generate_psbt(tmpl, reward_spk, *, blocktime=None):
- signet_spk = tmpl["signet_challenge"]
+def generate_psbt(block, signet_spk):
signet_spk_bin = bytes.fromhex(signet_spk)
+ signme, spendme = signet_txs(block, signet_spk_bin)
+ return do_createpsbt(block, signme, spendme)
+def new_block(tmpl, reward_spk, blocktime=None):
cbtx = create_coinbase(height=tmpl["height"], value=tmpl["coinbasevalue"], spk=reward_spk)
cbtx.vin[0].nSequence = 2**32-2
cbtx.rehash()
@@ -135,9 +138,10 @@ def generate_psbt(tmpl, reward_spk, *, blocktime=None):
block.vtx[0].wit.vtxinwit = [cbwit]
block.vtx[0].vout.append(CTxOut(0, bytes(get_witness_script(witroot, witnonce))))
- signme, spendme = signet_txs(block, signet_spk_bin)
+ block.vtx[0].rehash()
+ block.hashMerkleRoot = block.calc_merkle_root()
- return do_createpsbt(block, signme, spendme)
+ return block
def get_reward_address(args, height):
if args.address is not None:
@@ -177,8 +181,10 @@ def get_reward_addr_spk(args, height):
def do_genpsbt(args):
tmpl = json.load(sys.stdin)
+ signet_spk = tmpl["signet_challenge"]
_, reward_spk = get_reward_addr_spk(args, tmpl["height"])
- psbt = generate_psbt(tmpl, reward_spk)
+ block = new_block(tmpl, reward_spk)
+ psbt = generate_psbt(block, signet_spk)
print(psbt)
def do_solvepsbt(args):
@@ -407,14 +413,23 @@ def do_generate(args):
# mine block
logging.debug("Mining block delta=%s start=%s mine=%s", seconds_to_hms(mine_time-bestheader["time"]), mine_time, is_mine)
mined_blocks += 1
- psbt = generate_psbt(tmpl, reward_spk, blocktime=mine_time)
- input_stream = os.linesep.join([psbt, "true", "ALL"]).encode('utf8')
- psbt_signed = json.loads(args.bcli("-stdin", "walletprocesspsbt", input=input_stream))
- if not psbt_signed.get("complete",False):
- logging.debug("Generated PSBT: %s" % (psbt,))
- sys.stderr.write("PSBT signing failed\n")
- return 1
- block, signet_solution = do_decode_psbt(psbt_signed["psbt"])
+ block = new_block(tmpl, reward_spk, blocktime=mine_time)
+
+ # BIP325 allows omitting the signet commitment when scriptSig and
+ # scriptWitness are both empty. This is the case for trivial
+ # challenges such as OP_TRUE
+ signet_solution = None
+ signet_spk = tmpl["signet_challenge"]
+ if signet_spk != "51":
+ psbt = generate_psbt(block, signet_spk)
+ input_stream = os.linesep.join([psbt, "true", "ALL"]).encode('utf8')
+ psbt_signed = json.loads(args.bcli("-stdin", "walletprocesspsbt", input=input_stream))
+ if not psbt_signed.get("complete",False):
+ logging.debug("Generated PSBT: %s" % (psbt,))
+ sys.stderr.write("PSBT signing failed\n")
+ return 1
+ block, signet_solution = do_decode_psbt(psbt_signed["psbt"])
+
block = finish_block(block, signet_solution, args.grind_cmd)
# submit block
diff --git a/doc/REST-interface.md b/doc/REST-interface.md
index 2d7d0e3769367..6664bc2a3ae1d 100644
--- a/doc/REST-interface.md
+++ b/doc/REST-interface.md
@@ -4,7 +4,7 @@ Unauthenticated REST Interface
The REST API can be enabled with the `-rest` option.
The interface runs on the same port as the JSON-RPC interface, by default port 8332 for mainnet, port 18332 for testnet,
-port 38332 for signet, and port 18443 for regtest.
+port 48332 for testnet4, port 38332 for signet, and port 18443 for regtest.
REST Interface consistency guarantees
-------------------------------------
diff --git a/doc/files.md b/doc/files.md
index 03e52f02c928d..dd16548df51e7 100644
--- a/doc/files.md
+++ b/doc/files.md
@@ -34,12 +34,13 @@ Windows | `%LOCALAPPDATA%\Bitcoin\` [\[1\]](#note1)
3. All content of the data directory, except for `bitcoin.conf` file, is chain-specific. This means the actual data directory paths for non-mainnet cases differ:
-Chain option | Data directory path
--------------------------------|------------------------------
-`-chain=main` (default) | *path_to_datadir*`/`
-`-chain=test` or `-testnet` | *path_to_datadir*`/testnet3/`
-`-chain=signet` or `-signet` | *path_to_datadir*`/signet/`
-`-chain=regtest` or `-regtest` | *path_to_datadir*`/regtest/`
+Chain option | Data directory path
+---------------------------------|------------------------------
+`-chain=main` (default) | *path_to_datadir*`/`
+`-chain=test` or `-testnet` | *path_to_datadir*`/testnet3/`
+`-chain=testnet4` or `-testnet4` | *path_to_datadir*`/testnet4/`
+`-chain=signet` or `-signet` | *path_to_datadir*`/signet/`
+`-chain=regtest` or `-regtest` | *path_to_datadir*`/regtest/`
## Data directory layout
diff --git a/doc/release-process.md b/doc/release-process.md
index 1e6d49100ef7a..fa2c53eb0c506 100644
--- a/doc/release-process.md
+++ b/doc/release-process.md
@@ -311,13 +311,15 @@ Both variables are used as a guideline for how much space the user needs on thei
Note that all values should be taken from a **fully synced** node and have an overhead of 5-10% added on top of its base value.
To calculate `m_assumed_blockchain_size`, take the size in GiB of these directories:
-- For `mainnet` -> the data directory, excluding the `/testnet3`, `/signet`, and `/regtest` directories and any overly large files, e.g. a huge `debug.log`
+- For `mainnet` -> the data directory, excluding the `/testnet3`, `/testnet4`, `/signet`, and `/regtest` directories and any overly large files, e.g. a huge `debug.log`
- For `testnet` -> `/testnet3`
+- For `testnet4` -> `/testnet4`
- For `signet` -> `/signet`
To calculate `m_assumed_chain_state_size`, take the size in GiB of these directories:
- For `mainnet` -> `/chainstate`
- For `testnet` -> `/testnet3/chainstate`
+- For `testnet4` -> `/testnet4/chainstate`
- For `signet` -> `/signet/chainstate`
Notes:
diff --git a/doc/stratum-v2.md b/doc/stratum-v2.md
new file mode 100644
index 0000000000000..fab59d6d6911b
--- /dev/null
+++ b/doc/stratum-v2.md
@@ -0,0 +1,399 @@
+# Stratum v2
+
+## Design
+
+The Stratum v2 protocol specification can be found here: https://github.com/stratum-mining/sv2-spec
+
+Bitcoin Core performs the Template Provider role, and for that it implements the
+Template Distribution Protocol. When launched with `-sv2` we listen for connections
+from Job Declarator clients.
+
+A Job Declarator client might run on the same machine, e.g. for a single ASIC
+hobby miner. In a more advanced setup it might run on another machine, on the same
+local network or remote. A third possible use case is where a miner relies on a
+node run by someone else to provide the templates. Trust may not go both ways in
+that scenario, see the section on DoS.
+
+We send them a new block template whenenver out tip is updated, or when mempool
+fees have increased sufficiently. If the pool finds a block, we attempt to
+broadcast it based on a cached template.
+
+Communication with other roles uses the Noise Protocol, which has been implemented
+to the extend necessary. Its cryptographic primitives were chosen so that they
+were already present in the Bitcoin Core project at the time of writing the spec.
+
+### Advantage over getblocktemplate RPC
+
+Although under the hood the Template Provider uses `CreateNewBlock()` just like
+the `getblocktemplate` RPC, there's a number of advantages in running a
+server with a stateful connection, and avoiding JSON RPC in general.
+
+1. Stateful, so we can have back-and-forth, e.g. requesting transaction data,
+ processing a block solution.
+2. Less (de)serializing and data sent over the wire, compared to plain text JSON
+3. Encrypted, safer (for now: less unsafe) to expose on the public internet
+4. Push based: new template is sent immediately when a new block is found rather
+ than at the next poll interval. Combined with Cluster Mempool this can
+ hopefully be done for higher fee templates too.
+5. Low friction deployment with other Stratum v2 software / devices
+
+### Message flow(s)
+
+See the [Message Types](https://github.com/stratum-mining/sv2-spec/blob/main/08-Message-Types.md)
+and [Protocol Overview](https://github.com/stratum-mining/sv2-spec/blob/main/03-Protocol-Overview.md)
+section of the spec for all messages and their details.
+
+When a Job Declarator client connects to us, it first sends a `SetupConnection`
+message. We reply with `SetupConnection.Success` unless something went wrong,
+e.g. version mismatch, in which case we reply with `SetupConnection.Error`.
+
+Next the client sends us their `CoinbaseOutputDataSize`. If this is invalid we
+disconnect. Otherwise we start the cycle below that repeats with every block.
+
+We send a `NewTemplate` message with `future_template` set `true`, immedidately
+followed by `SetNewPrevHash`. We _don't_ send any transaction information
+at this point. The Job Declarator client uses this to announce upstream that
+it wants to declare a new template.
+
+In the simplest setup with SRI the Job Declarator client doubles as a proxy and
+sends these two messages to all connected mining devices. They will keep
+working on their previous job until the `SetNewPrevHash` message arrives.
+Future implementations could provide an empty or speculative template before
+a new block is found.
+
+Meanwhile the pool will request, via the Job Declarator client, the transaction
+lists belonging to the template: `RequestTransactionData`. In case of a problem
+we reply with `RequestTransactionData.Error`. Otherwise we reply with the full[0]
+transaction data in `RequestTransactionData.Success`.
+
+When we find a template with higher fees, we send a `NewTemplate` message
+with `future_template` set to `false`. This is _not_ followed by `SetNewPrevHash`.
+
+Finally, if we find an actual block, the client sends us `SubmitSolution`.
+We then lookup the template (may not be the most recent one), reconstruct
+the block and broadcast it. The pool will do the same.
+
+`[0]`: When the Job Declarator client communicates with the Job Declarator
+server there is an intermediate message which sends short transaction ids
+first, followed by a `ProvideMissingTransactions` message. The spec could be
+modified to introduce a similar message here. This is especially useful when
+the Template Provider runs on a different machine than the Job Declarator
+client. Erlay might be useful here too, in a later stage.
+
+### Noise Protocol
+
+As detailed in the [Protocol Security](https://github.com/stratum-mining/sv2-spec/blob/main/04-Protocol-Security.md)
+section of the spec, Stratum v2 roles use the Noise Protocol to communicate.
+
+We only implement the parts needed for inbound connections, although not much
+code would be needed to support outbound connections as well if this is required later.
+
+The spec was written before BIP 324 peer-to-peer encryption was introduced. It
+has much in common with Noise, but for the purposes of Stratum v2 it currently
+lacks authentication. Perhaps a future version of Stratum will use this. Since
+we only communicate with the Job Declarator role, a transition to BIP 324 would
+not require waiting for the entire mining ecosystem to adopt it.
+
+An alternative to implementing the Noise Protocol in Bitcoin Core is to use a
+unix socket instead and rely on the user to install a separate tool to convert
+to this protocol. Such a tool could be provided by developers of the Job
+Declarator client.
+
+ZMQ may be slightly more convenient than a unix socket. Since the Stratum v2
+protocol is stateful we would need to use the [request-reply](https://zguide.zeromq.org/docs/chapter3/)
+mode. Currently we only use the unidirectional `ZMQ_PUB` mode, see
+[zmq_socket](http://api.zeromq.org/4-2:zmq-socket). But then Stratum v2 messages
+can be sent and received without dealing with low level sockets / buffers / bytes.
+This could be implemented as a ZmqTransport subclass of Transport. Whether this
+involves less new code than the Noise Protocol remains to be seen.
+
+### Mempool monitoring
+
+The current design calls `CreateNewBlock()` internally every `-sv2interval` seconds.
+We then broadcast the resulting block template if fees have increased enough to make
+it worth the overhead (`-sv2feedelta`). A pool may have additional rate limiting in
+place.
+
+This is better than the Stratum v1 model of a polling call to the `getblocktemplate` RPC.
+It avoids (de)serializing JSON, uses an encrypted connection and only sends data
+over the wire if fees increased.
+
+But it's still a poll based model, as opposed to the push based approach
+whenever a new block arrives. It would be better if a new template is generated
+as soon as a potentially revenue-increasing transaction is added to the mempool.
+The Cluster Mempool project might enable that.
+
+### DoS and privacy
+
+The current Template Provider should not be run on the public internet with
+unlimited access. It is not harneded against DoS attacks, nor against mempool probing.
+
+There's currently no limit to the number of Job Declarator clients that can connect,
+which could exhaust memory. There's also no limit to the amount of raw transaction
+data that can be requested.
+
+Templates reveal what is in the mempool without any delay or randomization.
+
+This is why the use of `-sv2allowip` is required when `-sv2bind` is set to
+anything other than localhost on mainnet.
+
+Future improvements should aim to reduce or eliminate the above concerns such
+that any node can run a Template Provider as a public service.
+
+## Usage
+
+Using this in a production environment is not yet recommended, but see the testing guide below.
+
+### Parameters
+
+See also `bitcoind --help`.
+
+Start Bitcoin Core with `-sv2` to start a Template Provider server with default settings.
+The listening port can be changed with `-sv2port`.
+
+By default it only accepts connections from localhost. This can be changed
+using `-sv2bind`, which requires the use of `-sv2allowip`. See DoS and Privacy below.
+
+Use `-debug=sv2` to see Stratum v2 related log messages. Set `-loglevel=sv2:trace`
+to see which messages are exchanged with the Job Declarator client.
+
+The frequency at which new templates are generated can be controlled with
+`-sv2interval`. The new templates are only submitted to connected clients if
+they are for a new block, or if fees have increased by at least `-sv2feedelta`.
+
+You may increase `-sv2interval`` to something your node can handle, and then
+adjust `-sv2feedelta` to limit back and forth with the pool.
+
+You can use `-debug=bench` to see how long block generation typically takes on
+your machine, look for `CreateNewBlock() ... (total ...ms)`. Another factor to
+consider is upstream rate limiting, see the [Job Declaration Protocol](https://github.com/stratum-mining/sv2-spec/blob/main/06-Job-Declaration-Protocol.md).
+Mining hardware may also incur a performance dip when it receives a new job.
+
+## Testing Guide
+
+Unfortunately testing still requires quite a few moving parts, and each setup has
+its own merits and issues.
+
+To get help with the stratum side of things, this Discord may be useful: https://discord.gg/fsEW23wFYs
+
+The Stratum Reference Implementation (SRI) provides example implementations of
+the various (other) Stratum v2 roles: https://github.com/stratum-mining/stratum
+
+You can set up an entire pool on your own machine. You can also connect to an
+existing pool and only run a limited set of roles on your machine, e.g. the
+Job Declarator client and Translator (v1 to v2).
+
+SRI includes a v1 and v2 CPU miner, but at the time of writing neither seems to work.
+Another CPU miner that does work, when used with the Translator: https://github.com/pooler/cpuminer
+
+### Regtest
+
+TODO
+
+This is also needed for functional test suite coverage. It's also the only test
+network doesn't need a standalone CPU miner or ASIC.
+
+Perhaps a mock Job Declator client can be added. We also need a way mine a given
+block template, akin to `generate`.
+
+To make testing easier it should be possible to use a connection without Noise Protocol.
+
+### Testnet
+
+The difficulty on testnet3 varies wildly, but typically much too high for CPU mining.
+Even when using a relatively cheap second hand miner, e.g. an S9, it could take
+days to find a block.
+
+The above means it's difficult to test the `SubmitSolution` message.
+
+#### Bring your own ASIC, use external testnet pool
+
+The following is untested.
+
+This uses an existing testnet pool. There's no need to create an account anywhere.
+The pool does not pay out the testnet coins it generates. It also currently
+doesn't censor anything, so you can't test the (solo mining) fallback behavior.
+
+First start the node:
+
+```
+src/bitcoind -testnet -sv2 -debug=sv2
+```
+
+Build and run a Job Declator client: [stratum-mining/stratum/tree/main/roles/jd-client](https://github.com/stratum-mining/stratum/tree/main/roles/jd-client
+
+This client connects to your node to receive new block templates and then "declares"
+them to a Job Declarator server. Additionally it connects to the pool itself.
+Try this config (see documentation for more):
+
+```toml
+[[upstreams]]
+# Listen for connecting miner or translator:
+downstream_address = "127.0.0.1"
+downstream_port = 34265
+
+# Connect to upstream pool:
+authority_pubkey = "3VANfft6ei6jQq1At7d8nmiZzVhBFS4CiQujdgim1ign"
+pool_address = "75.119.150.111:34254"
+jd_address = "75.119.150.111:34264"
+pool_signature = "Stratum v2 SRI Pool"
+```
+
+Do not change the pool signature. It (currently) must match what the pool uses
+or you will mine invalid blocks.
+
+The `coinbase_outputs` is used for fallback to solo mining. Generate an address
+of any type and then use the `getaddressinfo` RPC to find its public key.
+
+Finally you most likely need to use the v1 to v2 translator: [stratum-mining/stratum/tree/main/roles/translator](https://github.com/stratum-mining/stratum/tree/main/roles/translator),
+even when you have a stratum v2 capable miner (see notes on ASIC's and Firmware below).
+
+You need to point the translator to your job declator client, which in turn takes
+care of connecting to the pool.
+
+```toml
+# Local SRI Testnet JDC Upstream Connection
+upstream_address = "127.0.0.1"
+upstream_port = 34265
+upstream_authority_pubkey = "3VANfft6ei6jQq1At7d8nmiZzVhBFS4CiQujdgim1ign"
+```
+
+The `upstream_authority_pubkey` field is required but ignored.
+
+As soon as you turn on the translator, the Bitcoin Core log should show a `SetupConnection` [message](https://github.com/stratum-mining/sv2-spec/blob/main/08-Message-Types.md).
+
+Now point your ASIC to the translator. At this point you should be seeing
+`NewTemplate`, `SetNewPrevHash` and `SetNewPrevHash` messages.
+
+If the pool is down, notify someone on the above mentioned Discord.
+
+### Custom Signet
+
+Unlike testnet3, signet(s) use the regular difficulty adjustment mechanism.
+Although the default signet has very low difficulty, you can't mine on it,
+because to do so requires signing blocks using a private key that only two people have.
+
+It's possible to create a signet that does not require signatures. There's no
+such public network, because it would risk being "attacked" by very powerful
+ASIC's. They could massively increase the difficulty and then disappear, making
+it impossible for a CPU miner to append new blocks.
+
+Instead, you can create your own custom unsigned signet. Unlike regtest this
+network does have difficulty (adjustment). This allows you to test if e.g. pool
+software correctly sets and adjusts the share difficulty for each participant.
+Although for the Template Provider role this is not relevant.
+
+#### Creating the signet
+
+See also [signet/README.md](../contrib/signet/README.md)
+
+If you use the default signet for anything else, create a fresh data directory.
+
+Add the following to `bitcoin.conf`:
+
+```ini
+[signet]
+# OP_TRUE
+signetchallenge=51
+```
+
+This challenge represents "the special case where an empty solution is valid
+(i.e. scriptSig and scriptWitness are both empty)", see [BIP 325](https://github.com/bitcoin/bips/blob/master/bip-0325.mediawiki). For mining software things will look just like testnet.
+
+The new chain needs to have at least 16 blocks, or the SRI software will panick.
+So we'll mine those using `bitcoin-util grind`:
+
+```sh
+CLI="src/bitcoin-cli"
+MINER="contrib/signet/miner"
+GRIND="src/bitcoin-util grind"
+ADDR=...
+NBITS=1d00ffff
+$MINER --cli="$CLI" generate --grind-cmd="$GRIND" --address="$ADDR" --nbits=$NBITS
+```
+
+#### Mining
+
+The cleanest setup involves two connected nodes, each with their own data
+directory: one for the pool and one for the miner. By selectively breaking the
+connection you can inspect how unknown transactions in the template are requested
+by the pool, and how a newly found block is submitted is submitted both by the
+pool and the miner.
+
+However things should work fine with just one node.
+
+Start the miner node first, with a GUI for convenience:
+
+```sh
+src/qt/bitcoin-qt -datadir=$HOME/.stratum/bitcoin -signet
+```
+
+Suggested config for the pool node:
+
+```ini
+[signet]
+# OP_TRUE
+signetchallenge=51
+server=0
+listen=0
+connect=127.0.0.1
+```
+
+The above disables its RPC server and p2p listening to avoid a port conflict.
+
+Start the pool node:
+
+```sh
+src/bitcoind -datadir=$HOME/.stratum/bitcoin-pool -signet
+```
+
+Configure an SRI pool:
+
+```
+cd roles/pool
+mkdir -p ~/.stratum
+cp
+```
+
+Start the SRI pool:
+
+```sh
+cargo run -p pool_sv2 -- -c ~/.stratum/signet-pool.toml
+```
+
+For the Job Declarator _client_ and Translator, see Testnet above.
+
+Now use the [CPU miner](https://github.com/pooler/cpuminer) and point it to the translator:
+
+```
+./minerd -a sha256d -o stratum+tcp://localhost:34255 -q -D -P
+```
+
+
+#### Mining after being away
+
+The Template Provider will not start until the node is caught up.
+Use `bitcoin-util grind` as explained above for it to catch up.
+
+### Mainnet
+
+See testnet for how to use an external pool. See signet for how to configure your own pool.
+
+Pools that support Stratum v2 on mainnet:
+
+* Braiins: unclear if they are currently compatible with latest spec. URL's are
+ listed [here](https://academy.braiins.com/en/braiins-pool/stratum-v2-manual/#servers-and-ports). There's no Job Declarator server.
+* DEMAND : No account needed for solo mining. Both the pool and Job Declarator
+ server are at `dmnd.work:2000`. Requires a custom SRI branch, see [instructions](https://dmnd.work/#solo-mine).
+
+### Notes on ASIC's and Firmware:
+
+#### BraiinsOS
+
+* v22.08.1 uses an (incompatible) older version of Stratum v2
+* v23.12 is untested (and not available on S9)
+* v22.08.1 when used in Stratum v1 mode, does not work with the SRI Translator
+
+#### Antminer stock OS
+
+This should work with the Translator, but has not been tested.
diff --git a/src/Makefile.am b/src/Makefile.am
index 72dd942c4012d..a5c21892b6acf 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -137,6 +137,10 @@ BITCOIN_CORE_H = \
common/bloom.h \
common/init.h \
common/run_command.h \
+ common/sv2_connman.h \
+ common/sv2_messages.h \
+ common/sv2_noise.h \
+ common/sv2_transport.h \
common/types.h \
common/url.h \
compat/assumptions.h \
@@ -234,6 +238,7 @@ BITCOIN_CORE_H = \
node/peerman_args.h \
node/protocol_version.h \
node/psbt.h \
+ node/sv2_template_provider.h \
node/timeoffsets.h \
node/transaction.h \
node/txreconciliation.h \
@@ -439,6 +444,7 @@ libbitcoin_node_a_SOURCES = \
node/minisketchwrapper.cpp \
node/peerman_args.cpp \
node/psbt.cpp \
+ node/sv2_template_provider.cpp \
node/timeoffsets.cpp \
node/transaction.cpp \
node/txreconciliation.cpp \
@@ -686,8 +692,12 @@ libbitcoin_common_a_SOURCES = \
common/interfaces.cpp \
common/messages.cpp \
common/run_command.cpp \
+ common/sv2_connman.cpp \
+ common/sv2_messages.cpp \
+ common/sv2_noise.cpp \
common/settings.cpp \
common/signmessage.cpp \
+ common/sv2_transport.cpp \
common/system.cpp \
common/url.cpp \
compressor.cpp \
diff --git a/src/Makefile.test.include b/src/Makefile.test.include
index 0993a65efff2f..b936eb488bc90 100644
--- a/src/Makefile.test.include
+++ b/src/Makefile.test.include
@@ -152,6 +152,11 @@ BITCOIN_TESTS =\
test/sock_tests.cpp \
test/span_tests.cpp \
test/streams_tests.cpp \
+ test/sv2_connman_tests.cpp \
+ test/sv2_messages_tests.cpp \
+ test/sv2_noise_tests.cpp \
+ test/sv2_template_provider_tests.cpp \
+ test/sv2_transport_tests.cpp \
test/sync_tests.cpp \
test/system_tests.cpp \
test/timeoffsets_tests.cpp \
@@ -388,6 +393,7 @@ test_fuzz_fuzz_SOURCES = \
test/fuzz/span.cpp \
test/fuzz/string.cpp \
test/fuzz/strprintf.cpp \
+ test/fuzz/sv2_noise.cpp \
test/fuzz/system.cpp \
test/fuzz/timeoffsets.cpp \
test/fuzz/torcontrol.cpp \
diff --git a/src/bitcoin-cli.cpp b/src/bitcoin-cli.cpp
index 44fc2731639b9..934b5fb6dc189 100644
--- a/src/bitcoin-cli.cpp
+++ b/src/bitcoin-cli.cpp
@@ -75,6 +75,7 @@ static void SetupCliArgs(ArgsManager& argsman)
const auto defaultBaseParams = CreateBaseChainParams(ChainType::MAIN);
const auto testnetBaseParams = CreateBaseChainParams(ChainType::TESTNET);
+ const auto testnet4BaseParams = CreateBaseChainParams(ChainType::TESTNET4);
const auto signetBaseParams = CreateBaseChainParams(ChainType::SIGNET);
const auto regtestBaseParams = CreateBaseChainParams(ChainType::REGTEST);
@@ -98,7 +99,7 @@ static void SetupCliArgs(ArgsManager& argsman)
argsman.AddArg("-rpcconnect=", strprintf("Send commands to node running on (default: %s)", DEFAULT_RPCCONNECT), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-rpccookiefile=", "Location of the auth cookie. Relative paths will be prefixed by a net-specific datadir location. (default: data dir)", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-rpcpassword=", "Password for JSON-RPC connections", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
- argsman.AddArg("-rpcport=", strprintf("Connect to JSON-RPC on (default: %u, testnet: %u, signet: %u, regtest: %u)", defaultBaseParams->RPCPort(), testnetBaseParams->RPCPort(), signetBaseParams->RPCPort(), regtestBaseParams->RPCPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::OPTIONS);
+ argsman.AddArg("-rpcport=", strprintf("Connect to JSON-RPC on (default: %u, testnet: %u, testnet4: %u, signet: %u, regtest: %u)", defaultBaseParams->RPCPort(), testnetBaseParams->RPCPort(), testnet4BaseParams->RPCPort(), signetBaseParams->RPCPort(), regtestBaseParams->RPCPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::OPTIONS);
argsman.AddArg("-rpcuser=", "Username for JSON-RPC connections", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-rpcwait", "Wait for RPC server to start", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-rpcwaittimeout=", strprintf("Timeout in seconds to wait for the RPC server to start, or 0 for no timeout. (default: %d)", DEFAULT_WAIT_CLIENT_TIMEOUT), ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::OPTIONS);
@@ -428,6 +429,8 @@ class NetinfoRequestHandler : public BaseRequestHandler
std::string ChainToString() const
{
switch (gArgs.GetChainType()) {
+ case ChainType::TESTNET4:
+ return " testnet4";
case ChainType::TESTNET:
return " testnet";
case ChainType::SIGNET:
diff --git a/src/bitcoin-wallet.cpp b/src/bitcoin-wallet.cpp
index b6f5c3f15d5db..7d030abe97e6a 100644
--- a/src/bitcoin-wallet.cpp
+++ b/src/bitcoin-wallet.cpp
@@ -69,7 +69,7 @@ static std::optional WalletAppInit(ArgsManager& args, int argc, char* argv[
strUsage += "\n"
"bitcoin-wallet is an offline tool for creating and interacting with " PACKAGE_NAME " wallet files.\n"
"By default bitcoin-wallet will act on wallets in the default mainnet wallet directory in the datadir.\n"
- "To change the target wallet, use the -datadir, -wallet and -regtest/-signet/-testnet arguments.\n\n"
+ "To change the target wallet, use the -datadir, -wallet and -regtest/-signet/-testnet/-testnet4 arguments.\n\n"
"Usage:\n"
" bitcoin-wallet [options] \n";
strUsage += "\n" + args.GetHelpMessage();
diff --git a/src/chainparams.cpp b/src/chainparams.cpp
index 5d4401b719c01..68319e8e8b258 100644
--- a/src/chainparams.cpp
+++ b/src/chainparams.cpp
@@ -115,6 +115,8 @@ std::unique_ptr CreateChainParams(const ArgsManager& args, c
return CChainParams::Main();
case ChainType::TESTNET:
return CChainParams::TestNet();
+ case ChainType::TESTNET4:
+ return CChainParams::TestNet4();
case ChainType::SIGNET: {
auto opts = CChainParams::SigNetOptions{};
ReadSigNetArgs(args, opts);
diff --git a/src/chainparamsbase.cpp b/src/chainparamsbase.cpp
index 8cbf9e85e0cad..6cceab74c9d84 100644
--- a/src/chainparamsbase.cpp
+++ b/src/chainparamsbase.cpp
@@ -17,7 +17,8 @@ void SetupChainParamsBaseOptions(ArgsManager& argsman)
argsman.AddArg("-regtest", "Enter regression test mode, which uses a special chain in which blocks can be solved instantly. "
"This is intended for regression testing tools and app development. Equivalent to -chain=regtest.", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-testactivationheight=name@height.", "Set the activation height of 'name' (segwit, bip34, dersig, cltv, csv). (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
- argsman.AddArg("-testnet", "Use the test chain. Equivalent to -chain=test.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
+ argsman.AddArg("-testnet", "Use the testnet3 chain. Equivalent to -chain=test. Support for testnet3 is deprecated and will be removed with the next release. Consider moving to testnet4 now by using -testnet4.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
+ argsman.AddArg("-testnet4", "Use the testnet4 chain. Equivalent to -chain=testnet4.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-vbparams=deployment:start:end[:min_activation_height]", "Use given start/end times and min_activation_height for specified version bits deployment (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-signet", "Use the signet chain. Equivalent to -chain=signet. Note that the network is defined by the -signetchallenge parameter", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-signetchallenge", "Blocks must satisfy the given script to be considered valid (only for signet networks; defaults to the global default signet test network challenge)", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::CHAINPARAMS);
@@ -33,20 +34,22 @@ const CBaseChainParams& BaseParams()
}
/**
- * Port numbers for incoming Tor connections (8334, 18334, 38334, 18445) have
+ * Port numbers for incoming Tor connections (8334, 18334, 38334, 48334, 18445) have
* been chosen arbitrarily to keep ranges of used ports tight.
*/
std::unique_ptr CreateBaseChainParams(const ChainType chain)
{
switch (chain) {
case ChainType::MAIN:
- return std::make_unique("", 8332, 8334);
+ return std::make_unique("", 8332, 8334, 8336);
case ChainType::TESTNET:
- return std::make_unique("testnet3", 18332, 18334);
+ return std::make_unique("testnet3", 18332, 18334, 18336);
+ case ChainType::TESTNET4:
+ return std::make_unique("testnet4", 48332, 48334, 48336);
case ChainType::SIGNET:
- return std::make_unique("signet", 38332, 38334);
+ return std::make_unique("signet", 38332, 38334, 38336);
case ChainType::REGTEST:
- return std::make_unique("regtest", 18443, 18445);
+ return std::make_unique("regtest", 18443, 18445, 18447);
}
assert(false);
}
diff --git a/src/chainparamsbase.h b/src/chainparamsbase.h
index ea933d1ca8322..bd0ceefcc4fde 100644
--- a/src/chainparamsbase.h
+++ b/src/chainparamsbase.h
@@ -22,14 +22,16 @@ class CBaseChainParams
const std::string& DataDir() const { return strDataDir; }
uint16_t RPCPort() const { return m_rpc_port; }
uint16_t OnionServiceTargetPort() const { return m_onion_service_target_port; }
+ uint16_t Sv2Port() const { return m_sv2_port; }
CBaseChainParams() = delete;
- CBaseChainParams(const std::string& data_dir, uint16_t rpc_port, uint16_t onion_service_target_port)
- : m_rpc_port(rpc_port), m_onion_service_target_port(onion_service_target_port), strDataDir(data_dir) {}
+ CBaseChainParams(const std::string& data_dir, uint16_t rpc_port, uint16_t onion_service_target_port, uint16_t sv2_port)
+ : m_rpc_port(rpc_port), m_onion_service_target_port(onion_service_target_port), m_sv2_port{sv2_port}, strDataDir(data_dir) {}
private:
const uint16_t m_rpc_port;
const uint16_t m_onion_service_target_port;
+ const uint16_t m_sv2_port;
std::string strDataDir;
};
diff --git a/src/common/args.cpp b/src/common/args.cpp
index caff36fdb3054..a37a16b62b02f 100644
--- a/src/common/args.cpp
+++ b/src/common/args.cpp
@@ -159,6 +159,7 @@ std::list ArgsManager::GetUnrecognizedSections() const
ChainTypeToString(ChainType::REGTEST),
ChainTypeToString(ChainType::SIGNET),
ChainTypeToString(ChainType::TESTNET),
+ ChainTypeToString(ChainType::TESTNET4),
ChainTypeToString(ChainType::MAIN),
};
@@ -773,10 +774,11 @@ std::variant ArgsManager::GetChainArg() const
const bool fRegTest = get_net("-regtest");
const bool fSigNet = get_net("-signet");
const bool fTestNet = get_net("-testnet");
+ const bool fTestNet4 = get_net("-testnet4");
const auto chain_arg = GetArg("-chain");
- if ((int)chain_arg.has_value() + (int)fRegTest + (int)fSigNet + (int)fTestNet > 1) {
- throw std::runtime_error("Invalid combination of -regtest, -signet, -testnet and -chain. Can use at most one.");
+ if ((int)chain_arg.has_value() + (int)fRegTest + (int)fSigNet + (int)fTestNet + (int)fTestNet4 > 1) {
+ throw std::runtime_error("Invalid combination of -regtest, -signet, -testnet, -testnet4 and -chain. Can use at most one.");
}
if (chain_arg) {
if (auto parsed = ChainTypeFromString(*chain_arg)) return *parsed;
@@ -786,6 +788,7 @@ std::variant ArgsManager::GetChainArg() const
if (fRegTest) return ChainType::REGTEST;
if (fSigNet) return ChainType::SIGNET;
if (fTestNet) return ChainType::TESTNET;
+ if (fTestNet4) return ChainType::TESTNET4;
return ChainType::MAIN;
}
diff --git a/src/common/args.h b/src/common/args.h
index 78a61313b91df..323a86d8dca3b 100644
--- a/src/common/args.h
+++ b/src/common/args.h
@@ -423,7 +423,7 @@ class ArgsManager
fs::path GetDataDir(bool net_specific) const;
/**
- * Return -regtest/-signet/-testnet/-chain= setting as a ChainType enum if a
+ * Return -regtest/-signet/-testnet/-testnet4/-chain= setting as a ChainType enum if a
* recognized chain type was set, or as a string if an unrecognized chain
* name was set. Raise an exception if an invalid combination of flags was
* provided.
diff --git a/src/common/sv2_connman.cpp b/src/common/sv2_connman.cpp
new file mode 100644
index 0000000000000..3906e9e37e8a5
--- /dev/null
+++ b/src/common/sv2_connman.cpp
@@ -0,0 +1,422 @@
+// Copyright (c) 2023-present The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include
+#include
+#include
+#include
+#include
+
+using node::Sv2MsgType;
+
+Sv2Connman::~Sv2Connman()
+{
+ AssertLockNotHeld(m_clients_mutex);
+
+ {
+ LOCK(m_clients_mutex);
+ for (const auto& client : m_sv2_clients) {
+ LogTrace(BCLog::SV2, "Disconnecting client id=%zu\n",
+ client->m_id);
+ client->m_disconnect_flag = true;
+ }
+ DisconnectFlagged();
+ }
+
+ Interrupt();
+ StopThreads();
+}
+
+bool Sv2Connman::Start(Sv2EventsInterface* msgproc, std::string host, uint16_t port)
+{
+ m_msgproc = msgproc;
+
+ try {
+ auto sock = BindListenPort(host, port);
+ m_listening_socket = std::move(sock);
+ } catch (const std::runtime_error& e) {
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Error, "Template Provider failed to bind to port %d: %s\n", port, e.what());
+ return false;
+ }
+
+ m_thread_sv2_handler = std::thread(&util::TraceThread, "sv2connman", [this] { ThreadSv2Handler(); });
+ return true;
+}
+
+std::shared_ptr Sv2Connman::BindListenPort(std::string host, uint16_t port) const
+{
+ const CService addr_bind = LookupNumeric(host, port);
+
+ auto sock = CreateSock(addr_bind.GetSAFamily(), SOCK_STREAM, IPPROTO_TCP);
+ if (!sock) {
+ throw std::runtime_error("Sv2 Template Provider cannot create socket");
+ }
+
+ struct sockaddr_storage sockaddr;
+ socklen_t len = sizeof(sockaddr);
+
+ if (!addr_bind.GetSockAddr(reinterpret_cast(&sockaddr), &len)) {
+ throw std::runtime_error("Sv2 Template Provider failed to get socket address");
+ }
+
+ if (sock->Bind(reinterpret_cast(&sockaddr), len) == SOCKET_ERROR) {
+ const int nErr = WSAGetLastError();
+ if (nErr == WSAEADDRINUSE) {
+ throw std::runtime_error(strprintf("Unable to bind to %d on this computer. Another Stratum v2 process is probably already running.\n", port));
+ }
+
+ throw std::runtime_error(strprintf("Unable to bind to %d on this computer (bind returned error %s )\n", port, NetworkErrorString(nErr)));
+ }
+
+ constexpr int max_pending_conns{4096};
+ if (sock->Listen(max_pending_conns) == SOCKET_ERROR) {
+ throw std::runtime_error("Sv2 listening socket has an error listening");
+ }
+
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Info, "%s listening on %s:%d\n", SV2_PROTOCOL_NAMES.at(m_subprotocol), host, port);
+
+ return sock;
+}
+
+
+void Sv2Connman::DisconnectFlagged()
+{
+ AssertLockHeld(m_clients_mutex);
+
+ // Remove clients that are flagged for disconnection.
+ m_sv2_clients.erase(
+ std::remove_if(m_sv2_clients.begin(), m_sv2_clients.end(), [](const auto &client) {
+ return client->m_disconnect_flag;
+ }), m_sv2_clients.end());
+}
+
+void Sv2Connman::ThreadSv2Handler() EXCLUSIVE_LOCKS_REQUIRED(!m_clients_mutex)
+{
+ AssertLockNotHeld(m_clients_mutex);
+
+ while (!m_flag_interrupt_sv2) {
+ {
+ LOCK(m_clients_mutex);
+ DisconnectFlagged();
+ }
+
+ // Poll/Select the sockets that need handling.
+ Sock::EventsPerSock events_per_sock = WITH_LOCK(m_clients_mutex, return GenerateWaitSockets(m_listening_socket, m_sv2_clients));
+
+ constexpr auto timeout = std::chrono::milliseconds(50);
+ if (!events_per_sock.begin()->first->WaitMany(timeout, events_per_sock)) {
+ continue;
+ }
+
+ // Accept any new connections for sv2 clients.
+ const auto listening_sock = events_per_sock.find(m_listening_socket);
+ if (listening_sock != events_per_sock.end() && listening_sock->second.occurred & Sock::RECV) {
+ struct sockaddr_storage sockaddr;
+ socklen_t sockaddr_len = sizeof(sockaddr);
+
+ auto sock = m_listening_socket->Accept(reinterpret_cast(&sockaddr), &sockaddr_len);
+ if (sock) {
+ Assume(m_certificate);
+ LOCK(m_clients_mutex);
+ std::unique_ptr transport = std::make_unique(m_static_key, m_certificate.value());
+ size_t id{m_sv2_clients.size() + 1};
+ auto client = std::make_unique(id, std::move(sock), std::move(transport));
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "New client id=%zu connected\n", client->m_id);
+ m_sv2_clients.emplace_back(std::move(client));
+ }
+ }
+
+ LOCK(m_clients_mutex);
+ // Process messages from and for connected sv2_clients.
+ for (auto& client : m_sv2_clients) {
+ bool has_received_data = false;
+ bool has_error_occurred = false;
+
+ const auto socket_it = events_per_sock.find(client->m_sock);
+ if (socket_it != events_per_sock.end()) {
+ has_received_data = socket_it->second.occurred & Sock::RECV;
+ has_error_occurred = socket_it->second.occurred & Sock::ERR;
+ }
+
+ if (has_error_occurred) {
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Socket receive error, disconnecting client id=%zu\n",
+ client->m_id);
+ client->m_disconnect_flag = true;
+ continue;
+ }
+
+ // Process message queue and any outbound bytes still held by the transport
+ auto it = client->m_send_messages.begin();
+ std::optional expected_more;
+ while(true) {
+ if (it != client->m_send_messages.end()) {
+ // If possible, move one message from the send queue to the transport.
+ // This fails when there is an existing message still being sent,
+ // or when the handshake has not yet completed.
+ //
+ // Wrap Sv2NetMsg inside CSerializedNetMsg for transport
+ CSerializedNetMsg net_msg{*it};
+ if (client->m_transport->SetMessageToSend(net_msg)) {
+ ++it;
+ }
+ }
+
+ const auto& [data, more, _m_message_type] = client->m_transport->GetBytesToSend(/*have_next_message=*/it != client->m_send_messages.end());
+ size_t total_sent = 0;
+
+ // We rely on the 'more' value returned by GetBytesToSend to correctly predict whether more
+ // bytes are still to be sent, to correctly set the MSG_MORE flag. As a sanity check,
+ // verify that the previously returned 'more' was correct.
+ if (expected_more.has_value()) Assume(!data.empty() == *expected_more);
+ expected_more = more;
+ ssize_t sent = 0;
+
+ if (!data.empty()) {
+ int flags = MSG_NOSIGNAL | MSG_DONTWAIT;
+#ifdef MSG_MORE
+ if (more) {
+ flags |= MSG_MORE;
+ }
+#endif
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Send %d bytes to client id=%zu\n",
+ data.size() - total_sent, client->m_id);
+ sent = client->m_sock->Send(data.data() + total_sent, data.size() - total_sent, flags);
+ }
+ if (sent > 0) {
+ // Notify transport that bytes have been processed.
+ client->m_transport->MarkBytesSent(sent);
+ if ((size_t)sent != data.size()) {
+ // could not send full message; stop sending more
+ break;
+ }
+ } else {
+ if (sent < 0) {
+ // error
+ int nErr = WSAGetLastError();
+ if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS) {
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Debug, "Socket send error for client id=%zu: %s\n",
+ client->m_id, NetworkErrorString(nErr));
+ client->m_disconnect_flag = true;
+ }
+ }
+ break;
+ }
+ }
+ // Clear messages that have been handed to transport from the queue
+ client->m_send_messages.erase(client->m_send_messages.begin(), it);
+
+ // Stop processing this client if something went wrong during sending
+ if (client->m_disconnect_flag) break;
+
+ if (has_received_data) {
+ uint8_t bytes_received_buf[0x10000];
+
+ const auto num_bytes_received = client->m_sock->Recv(bytes_received_buf, sizeof(bytes_received_buf), MSG_DONTWAIT);
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Num bytes received from client id=%zu: %d\n",
+ client->m_id, num_bytes_received);
+
+ if (num_bytes_received <= 0) {
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Disconnecting client id=%zu\n",
+ client->m_id);
+ client->m_disconnect_flag = true;
+ break;
+ }
+
+ try
+ {
+ auto msg_ = Span(bytes_received_buf, num_bytes_received);
+ Span msg(reinterpret_cast(msg_.data()), msg_.size());
+ while (msg.size() > 0) {
+ // absorb network data
+ if (!client->m_transport->ReceivedBytes(msg)) {
+ // Serious transport problem
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Transport problem, disconnecting client id=%zu\n",
+ client->m_id);
+ client->m_disconnect_flag = true;
+ break;
+ }
+
+ if (client->m_transport->ReceivedMessageComplete()) {
+ bool dummy_reject_message = false;
+ Sv2NetMsg msg = client->m_transport->GetReceivedMessage(std::chrono::milliseconds(0), dummy_reject_message);
+ ProcessSv2Message(msg, *client.get());
+ }
+ }
+ } catch (const std::exception& e) {
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Error, "Received error when processing client id=%zu message: %s\n", client->m_id, e.what());
+ client->m_disconnect_flag = true;
+ }
+ }
+ }
+ }
+}
+
+Sock::EventsPerSock Sv2Connman::GenerateWaitSockets(const std::shared_ptr& listen_socket, const Clients& sv2_clients) const
+{
+ Sock::EventsPerSock events_per_sock;
+ events_per_sock.emplace(listen_socket, Sock::Events(Sock::RECV));
+
+ for (const auto& client : sv2_clients) {
+ if (!client->m_disconnect_flag && client->m_sock) {
+ events_per_sock.emplace(client->m_sock, Sock::Events{Sock::RECV | Sock::ERR});
+ }
+ }
+
+ return events_per_sock;
+}
+
+void Sv2Connman::Interrupt()
+{
+ m_flag_interrupt_sv2 = true;
+}
+
+void Sv2Connman::StopThreads()
+{
+ if (m_thread_sv2_handler.joinable()) {
+ m_thread_sv2_handler.join();
+ }
+}
+
+void Sv2Connman::ProcessSv2Message(const Sv2NetMsg& sv2_net_msg, Sv2Client& client)
+{
+ uint8_t msg_type[1] = {uint8_t(sv2_net_msg.m_msg_type)};
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Debug, "Received 0x%s %s from client id=%zu\n",
+ // After clang-17:
+ // std::format("{:x}", uint8_t(sv2_net_msg.m_msg_type)),
+ HexStr(msg_type),
+ node::SV2_MSG_NAMES.at(sv2_net_msg.m_msg_type), client.m_id);
+
+ DataStream ss (sv2_net_msg.m_msg);
+
+ switch (sv2_net_msg.m_msg_type)
+ {
+ case Sv2MsgType::SETUP_CONNECTION:
+ {
+ if (client.m_setup_connection_confirmed) {
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Error, "Client client id=%zu connection has already been confirmed\n",
+ client.m_id);
+ return;
+ }
+
+ node::Sv2SetupConnectionMsg setup_conn;
+ try {
+ ss >> setup_conn;
+ } catch (const std::exception& e) {
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Error, "Received invalid SetupConnection message from client id=%zu: %s\n",
+ client.m_id, e.what());
+ client.m_disconnect_flag = true;
+ return;
+ }
+
+ // Disconnect a client that connects on the wrong subprotocol.
+ if (setup_conn.m_protocol != m_subprotocol) {
+ node::Sv2SetupConnectionErrorMsg setup_conn_err{setup_conn.m_flags, std::string{"unsupported-protocol"}};
+
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Debug, "Send 0x02 SetupConnectionError to client id=%zu\n",
+ client.m_id);
+ client.m_send_messages.emplace_back(setup_conn_err);
+
+ client.m_disconnect_flag = true;
+ return;
+ }
+
+ // Disconnect a client if they are not running a compatible protocol version.
+ if ((m_protocol_version < setup_conn.m_min_version) || (m_protocol_version > setup_conn.m_max_version)) {
+ node::Sv2SetupConnectionErrorMsg setup_conn_err{setup_conn.m_flags, std::string{"protocol-version-mismatch"}};
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Debug, "Send 0x02 SetupConnection.Error to client id=%zu\n",
+ client.m_id);
+ client.m_send_messages.emplace_back(setup_conn_err);
+
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Error, "Received a connection from client id=%zu with incompatible protocol_versions: min_version: %d, max_version: %d\n",
+ client.m_id, setup_conn.m_min_version, setup_conn.m_max_version);
+ client.m_disconnect_flag = true;
+ return;
+ }
+
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Debug, "Send 0x01 SetupConnection.Success to client id=%zu\n",
+ client.m_id);
+ node::Sv2SetupConnectionSuccessMsg setup_success{m_protocol_version, m_optional_features};
+ client.m_send_messages.emplace_back(setup_success);
+
+ client.m_setup_connection_confirmed = true;
+
+ break;
+ }
+ case Sv2MsgType::COINBASE_OUTPUT_DATA_SIZE:
+ {
+ if (!client.m_setup_connection_confirmed) {
+ client.m_disconnect_flag = true;
+ return;
+ }
+
+ node::Sv2CoinbaseOutputDataSizeMsg coinbase_output_data_size;
+ try {
+ ss >> coinbase_output_data_size;
+ client.m_coinbase_output_data_size_recv = true;
+ } catch (const std::exception& e) {
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Error, "Received invalid CoinbaseOutputDataSize message from client id=%zu: %s\n",
+ client.m_id, e.what());
+ client.m_disconnect_flag = true;
+ return;
+ }
+
+ uint32_t max_additional_size = coinbase_output_data_size.m_coinbase_output_max_additional_size;
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Debug, "coinbase_output_max_additional_size=%d bytes\n", max_additional_size);
+
+ if (max_additional_size > MAX_BLOCK_WEIGHT) {
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Error, "Received impossible CoinbaseOutputDataSize from client id=%zu: %d\n",
+ client.m_id, max_additional_size);
+ client.m_disconnect_flag = true;
+ return;
+ }
+
+ client.m_coinbase_tx_outputs_size = coinbase_output_data_size.m_coinbase_output_max_additional_size;
+
+ break;
+ }
+ case Sv2MsgType::SUBMIT_SOLUTION: {
+ if (!client.m_setup_connection_confirmed && !client.m_coinbase_output_data_size_recv) {
+ client.m_disconnect_flag = true;
+ return;
+ }
+
+ node::Sv2SubmitSolutionMsg submit_solution;
+ try {
+ ss >> submit_solution;
+ } catch (const std::exception& e) {
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Error, "Received invalid SubmitSolution message from client id=%zu: %e\n",
+ client.m_id, e.what());
+ return;
+ }
+
+ m_msgproc->SubmitSolution(submit_solution);
+
+ break;
+ }
+ case Sv2MsgType::REQUEST_TRANSACTION_DATA:
+ {
+ node::Sv2RequestTransactionDataMsg request_tx_data;
+
+ try {
+ ss >> request_tx_data;
+ } catch (const std::exception& e) {
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Error, "Received invalid RequestTransactionData message from client id=%zu: %e\n",
+ client.m_id, e.what());
+ return;
+ }
+
+ m_msgproc->RequestTransactionData(client, request_tx_data);
+
+ break;
+ }
+ default: {
+ uint8_t msg_type[1]{uint8_t(sv2_net_msg.m_msg_type)};
+ LogPrintLevel(BCLog::SV2, BCLog::Level::Warning, "Received unknown message type 0x%s from client id=%zu\n",
+ HexStr(msg_type), client.m_id);
+ break;
+ }
+ }
+
+ m_msgproc->ReceivedMessage(client, sv2_net_msg.m_msg_type);
+}
diff --git a/src/common/sv2_connman.h b/src/common/sv2_connman.h
new file mode 100644
index 0000000000000..871377f6ce02e
--- /dev/null
+++ b/src/common/sv2_connman.h
@@ -0,0 +1,245 @@
+// Copyright (c) 2023-present The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#ifndef BITCOIN_COMMON_SV2_CONNMAN_H
+#define BITCOIN_COMMON_SV2_CONNMAN_H
+
+#include
+#include
+#include
+
+namespace {
+ /*
+ * Supported Stratum v2 subprotocols
+ */
+ static constexpr uint8_t TP_SUBPROTOCOL{0x02};
+
+ static const std::map SV2_PROTOCOL_NAMES{
+ {0x02, "Template Provider"},
+ };
+}
+
+struct Sv2Client
+{
+ /* Ephemeral identifier for debugging purposes */
+ size_t m_id;
+
+ /**
+ * Receiving and sending socket for the connected client
+ */
+ std::shared_ptr m_sock;
+
+ /**
+ * Transport
+ */
+ std::unique_ptr m_transport;
+
+ /**
+ * Whether the client has confirmed the connection with a successful SetupConnection.
+ */
+ bool m_setup_connection_confirmed = false;
+
+ /**
+ * Whether the client is a candidate for disconnection.
+ */
+ bool m_disconnect_flag = false;
+
+ /** Queue of messages to be sent */
+ std::deque m_send_messages;
+
+ /**
+ * Whether the client has received CoinbaseOutputDataSize message.
+ */
+ bool m_coinbase_output_data_size_recv = false;
+
+ /**
+ * Specific additional coinbase tx output size required for the client.
+ */
+ unsigned int m_coinbase_tx_outputs_size;
+
+ explicit Sv2Client(size_t id, std::shared_ptr sock, std::unique_ptr transport) :
+ m_id{id}, m_sock{std::move(sock)}, m_transport{std::move(transport)} {};
+
+ bool IsFullyConnected()
+ {
+ return !m_disconnect_flag && m_setup_connection_confirmed;
+ }
+
+ Sv2Client(Sv2Client&) = delete;
+ Sv2Client& operator=(const Sv2Client&) = delete;
+};
+
+/**
+ * Interface for sv2 message handling
+ */
+class Sv2EventsInterface
+{
+public:
+ /**
+ * Generic notification that a message was received. Does not include the
+ * message itself.
+ *
+ * @param[in] client The client which we have received messages from.
+ * @param[in] msg_type the message type
+ */
+ virtual void ReceivedMessage(Sv2Client& client, node::Sv2MsgType msg_type) = 0;
+
+ /**
+ * We received and successfully parsed a RequestTransactionData message.
+ * Deal with it and respond with either RequestTransactionData.Success or
+ * RequestTransactionData.Error.
+ */
+ virtual void RequestTransactionData(Sv2Client& client, node::Sv2RequestTransactionDataMsg msg) = 0;
+
+ /**
+ * We received and successfully parsed a SubmitSolution message.
+ */
+ virtual void SubmitSolution(node::Sv2SubmitSolutionMsg solution) = 0;
+
+ virtual ~Sv2EventsInterface() = default;
+};
+
+/*
+ * Handle Stratum v2 connections, similar to CConnman.
+ * Currently only supports inbound connections.
+ */
+class Sv2Connman
+{
+private:
+ /** Interface to pass events up */
+ Sv2EventsInterface* m_msgproc;
+
+ /**
+ * The current protocol version of stratum v2 supported by the server. Not to be confused
+ * with byte value of identitying the stratum v2 subprotocol.
+ */
+ const uint16_t m_protocol_version = 2;
+
+ /**
+ * The currently supported optional features.
+ */
+ const uint16_t m_optional_features = 0;
+
+ /**
+ * The subprotocol used in setup connection messages.
+ * An Sv2Connman only recognizes its own subprotocol.
+ */
+ const uint8_t m_subprotocol;
+
+ /**
+ * The main listening socket for new stratum v2 connections.
+ */
+ std::shared_ptr m_listening_socket;
+
+ CKey m_static_key;
+
+ XOnlyPubKey m_authority_pubkey;
+
+ std::optional m_certificate;
+
+ /**
+ * A list of all connected stratum v2 clients.
+ */
+ using Clients = std::vector>;
+ Clients m_sv2_clients GUARDED_BY(m_clients_mutex);
+
+ /**
+ * The main thread for connection handling.
+ */
+ std::thread m_thread_sv2_handler;
+
+ /**
+ * Signal for handling interrupts and stopping the template provider event loop.
+ */
+ std::atomic m_flag_interrupt_sv2{false};
+ CThreadInterrupt m_interrupt_sv2;
+
+ /**
+ * Creates a socket and binds the port for new stratum v2 connections.
+ * @throws std::runtime_error if port is unable to bind.
+ */
+ [[nodiscard]] std::shared_ptr BindListenPort(std::string host, uint16_t port) const;
+
+ void DisconnectFlagged() EXCLUSIVE_LOCKS_REQUIRED(m_clients_mutex);
+
+ /**
+ * The main thread for the connection manager, contains an event loop handling
+ * all tasks for it.
+ */
+ void ThreadSv2Handler();
+
+ /**
+ * Generates the socket events for each Sv2Client socket and the main listening socket.
+ */
+ [[nodiscard]] Sock::EventsPerSock GenerateWaitSockets(const std::shared_ptr& listen_socket, const Clients& sv2_clients) const;
+
+ /**
+ * Encrypt the header and message payload and send it.
+ * @throws std::runtime_error if encrypting the message fails.
+ */
+ bool EncryptAndSendMessage(Sv2Client& client, node::Sv2NetMsg& net_msg);
+
+ /**
+ * A helper method to read and decrypt multiple Sv2NetMsgs.
+ */
+ std::vector ReadAndDecryptSv2NetMsgs(Sv2Client& client, Span buffer);
+
+public:
+ Sv2Connman(uint8_t subprotocol, CKey static_key, XOnlyPubKey authority_pubkey, Sv2SignatureNoiseMessage certificate) :
+ m_subprotocol(subprotocol), m_static_key(static_key), m_authority_pubkey(authority_pubkey), m_certificate(certificate) {};
+
+ ~Sv2Connman();
+
+ Mutex m_clients_mutex;
+
+ /**
+ * Starts the Stratum v2 server and thread.
+ * returns false if port is unable to bind.
+ */
+ [[nodiscard]] bool Start(Sv2EventsInterface* msgproc, std::string host, uint16_t port);
+
+ /**
+ * Triggered on interrupt signals to stop the main event loop in ThreadSv2Handler().
+ */
+ void Interrupt();
+
+ /**
+ * Tear down of the connman thread and any other necessary tear down.
+ */
+ void StopThreads();
+
+ /**
+ * Main handler for all received stratum v2 messages.
+ */
+ void ProcessSv2Message(const node::Sv2NetMsg& sv2_header, Sv2Client& client);
+
+ using Sv2ClientFn = std::function;
+ /** Perform a function on each fully connected client. */
+ void ForEachClient(const Sv2ClientFn& func) EXCLUSIVE_LOCKS_REQUIRED(!m_clients_mutex)
+ {
+ LOCK(m_clients_mutex);
+ for (const auto& client : m_sv2_clients) {
+ if (client->IsFullyConnected()) func(*client);
+ }
+ };
+
+ /** Number of clients that are not marked for disconnection, used for tests. */
+ size_t ConnectedClients() EXCLUSIVE_LOCKS_REQUIRED(m_clients_mutex)
+ {
+ return std::count_if(m_sv2_clients.begin(), m_sv2_clients.end(), [](const auto& c) {
+ return !c->m_disconnect_flag;
+ });
+ }
+
+ /** Number of clients with m_setup_connection_confirmed, used for tests. */
+ size_t FullyConnectedClients() EXCLUSIVE_LOCKS_REQUIRED(m_clients_mutex)
+ {
+ return std::count_if(m_sv2_clients.begin(), m_sv2_clients.end(), [](const auto& c) {
+ return c->IsFullyConnected();
+ });
+ }
+
+};
+
+#endif // BITCOIN_COMMON_SV2_CONNMAN_H
diff --git a/src/common/sv2_messages.cpp b/src/common/sv2_messages.cpp
new file mode 100644
index 0000000000000..2b429040a2059
--- /dev/null
+++ b/src/common/sv2_messages.cpp
@@ -0,0 +1,41 @@
+#include
+
+#include
+#include
+#include
+
+node::Sv2NewTemplateMsg::Sv2NewTemplateMsg(const CBlockHeader& header, const CTransactionRef coinbase_tx, std::vector coinbase_merkle_path, int witness_commitment_index, uint64_t template_id, bool future_template)
+ : m_template_id{template_id}, m_future_template{future_template}
+{
+ m_version = header.nVersion;
+
+ m_coinbase_tx_version = coinbase_tx->CURRENT_VERSION;
+ m_coinbase_prefix = coinbase_tx->vin[0].scriptSig;
+ m_coinbase_tx_input_sequence = coinbase_tx->vin[0].nSequence;
+
+ // The coinbase nValue already contains the nFee + the Block Subsidy when built using CreateBlock().
+ m_coinbase_tx_value_remaining = static_cast(coinbase_tx->vout[0].nValue);
+
+ m_coinbase_tx_outputs_count = 0;
+ if (witness_commitment_index != NO_WITNESS_COMMITMENT) {
+ m_coinbase_tx_outputs_count = 1;
+
+ std::vector coinbase_tx_outputs{coinbase_tx->vout[witness_commitment_index]};
+ m_coinbase_tx_outputs = coinbase_tx_outputs;
+ }
+
+ m_coinbase_tx_locktime = coinbase_tx->nLockTime;
+
+ for (const auto& hash : coinbase_merkle_path) {
+ m_merkle_path.push_back(hash);
+ }
+
+}
+
+node::Sv2SetNewPrevHashMsg::Sv2SetNewPrevHashMsg(const CBlockHeader& header, uint64_t template_id) : m_template_id{template_id}
+{
+ m_prev_hash = header.hashPrevBlock;
+ m_header_timestamp = header.nTime;
+ m_nBits = header.nBits;
+ m_target = ArithToUint256(arith_uint256().SetCompact(header.nBits));
+}
diff --git a/src/common/sv2_messages.h b/src/common/sv2_messages.h
new file mode 100644
index 0000000000000..0ad3f3b0b385a
--- /dev/null
+++ b/src/common/sv2_messages.h
@@ -0,0 +1,719 @@
+// Copyright (c) 2023-present The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#ifndef BITCOIN_COMMON_SV2_MESSAGES_H
+#define BITCOIN_COMMON_SV2_MESSAGES_H
+
+#include // for CSerializedNetMsg and CNetMessage
+#include
+#include
+#include
+#include