https://github.com/theStack/bitcoin-inquisition/blob/81f7845d676c17dee3774c2d4a83954a59d4bf31/test/functional/feature_opcat.py#L141-L147
To turn this into variable pow, I think it's probably easiest to just hard code the delay and pow requirement: if you start at difficulty 2**46, that's higher than mainnet, then halve every 10 blocks, you're down to difficulty 1 after about three days. Halving the difficulty is a little complicated: you need `H = HA || HB || HC` where HC is all zeroes, HB is one byte, not negative, and less than or equal to [127, 63, 31, 15, 7, 3, 1, 0].
```python
def faucat_script(n):
assert 0 <= n < 47
delay = (47-n)*10
zeroes = b'\0' * (n//8 + 4)
prefix = [127, 63, 31, 15, 7, 3, 1, 0][n%8]
return CScript([
delay, OP_CSV, OP_DROP, # check delay (256*n + HC)
OP_DUP, prefix, OP_LESSTHANOREQUAL, OP_VERIFY, # prefix is below targer
OP_DUP, OP_SIZE, 1, OP_EQUALVERIFY, 0, OP_GREATERTHANOREQUAL, OP_VERIFY, # no funny business!
zeroes, OP_CAT, OP_CAT, OP_TOALTSTACK, # save H = HA || HB || HC
OP_OVER, OP_SIZE, 64, OP_EQUALVERIFY # grab signature, check it's 64B
OP_CAT, OP_SHA256, # re-calculate hash, , i.e. sha256(nonce || sig) == H
OP_FROMALTSTACK, OP_EQUALVERIFY, # check PoW
OP_SWAP, OP_SIZE, 32, OP_EQUALVERIFY, # check pubkey is 32B, no funny business!
OP_CHECKSIG, # verify signature
])
```
Then construct a taptree with 47 elements and you have a static address. There's probably silly bugs in the above, but in theory it commits to the difficulty target, so to spend these funds, you'd first sign, then pick an `n`, then mine. Would probably make sense to have a balanced taptree of 64 leaves and allow dropping to a lower difficulty than 1.
One thing that might be interesting: if you construct the data being hashed as "X SIG Y", and used "HASH256", people could make X be 4 bytes, and Y be 12 bytes, and probably would then be able to use regular mining software/hardware to grind an answer, treating the signature as the prev-block-hash and merkle-tree-root value.
Oh, if you're requiring sighash_default and using different tapscripts for each difficulty, the signature will commit to the target difficulty automatically and can't be reused against a different difficulty in case you're lucky and got a particularly good result.
Thanks, this is fun! :cat: Pushed some changes based on your suggestions:
* "no funny business" checks: enforce 32-bytes pubkey size, 64-byte signature size (i.e. SIGHASH_DEFAULT);
* fixed a bug in script-path spending, where the sign bit in the control block / leaf version byte wasn't taken into account, so every now and then a "witness program hash mismatch" error appeared (wasted many hours on this, searching on the wrong place, d'oh! :man_facepalming: )
* took over the enhanced script in the opening post for variable pow, with the script builder
There is still tons of TODOs and it desperately needs more tests (it's just trying the success case with fixed n=17 now, where n=0 means all hashes pass for regtest purposes), but it's a start. I thought that H_B needs to be either zero or one bytes, as the integer 0 has to be encoded as empty byte-string (minimal-encoding rules), but as I'm writing this I'm not so sure anymore. Maybe the prefix table just has to start with value 255 and it's just fine. Will give it more thoughts tomorrow and possibly address other suggestions (being able to use regular mining software would be awesome), further feedback appreciated of course.
So, umm, :sweat_smile:
> I thought that H_B needs to be either zero or one bytes,
My theory was that you'd just choose a difficulty where your H_B was the first non-zero, but obviously committing to the difficulty in the signature breaks that. So here's a script that calculates difficulty by hand. Top of stack is "difficulty", a CScriptNum from 0 to 63, second from top is your first non-zero byte, third from top is the rest.
```
DUP
DUP ADD DUP DUP ADD DUP DUP ADD ADD # multiply top of stack by 10
640 SWAP SUB # subtract from 640
CHECKSEQUENCEVERIFY DROP # wait that many blocks
# put the 00-bytes onto the alt stack, one zero byte for every multiple of 8 in the difficulty
0 TOALTSTACK
DUP 32 GREATERTHANOREQUAL IF 32 SUB 0x0400000000 FROMALTSTACK CAT TOALTSTACK ENDIF
DUP 16 GREATERTHANOREQUAL IF 16 SUB 0x020000 FROMALTSTACK CAT TOALTSTACK ENDIF
DUP 8 GREATERTHANOREQUAL IF 8 SUB 0x0100 FROMALTSTACK CAT TOALTSTACK ENDIF
# difficulty should be 0-7 now; 0 means below 0x80, 1 means below 0x40, .., 6 means below 0x02, 7 means 0x00 exactly
DUP 0 EQUAL IF 127 TOALTSTACK ENDIF 1SUB
DUP 0 EQUAL IF 63 TOALTSTACK ENDIF 1SUB
DUP 0 EQUAL IF 31 TOALTSTACK ENDIF 1SUB
DUP 0 EQUAL IF 15 TOALTSTACK ENDIF 1SUB
DUP 0 EQUAL IF 7 TOALTSTACK ENDIF 1SUB
DUP 0 EQUAL IF 3 TOALTSTACK ENDIF 1SUB
DUP 0 EQUAL IF 1 TOALTSTACK ENDIF 1SUB
0 GREATERTHANOREQUAL IF
# avoid dealing with negatives when the first non-zero byte is 0x80-0xFF
# by accepting a 0x00 byte exactly
SIZE 1 EQUALVERIFY
DUP 0x00 EQUALVERIFY
ELSE
SIZE 1 EQUALVERIFY
DUP 0 GREATERTHAN VERIFY
DUP FROMALTSTACK LESSTHANOREQUAL VERIFY
ENDIF
FROMALTSTACK CAT CAT
```
That leaves you with the merged hash on the top of the stack, having verified that an appropriate CSV delay has completed, but you still need to put the preimage of the hash together.
Alright, another try with the new suggestion: https://github.com/theStack/bitcoin-inquisition/commit/e3cabf0303bb0ffca267f5f896d628a4a028232a
> Oh, if you're requiring sighash_default and using different tapscripts for each difficulty, the signature will commit to the target difficulty automatically and can't be reused against a different difficulty in case you're lucky and got a particularly good result.
But hmm, I think even if the difficulty is not hard-coded in the witness script, the signature still commits to the difficulty by the input's nSequence field that needs to be set for OP_CSV. If one got lucky and has a better result than intended, the whole point of that would be to claim the funds faster, which means setting a lower nSequence, resulting in a different signature hash.
The "dealing with negatives" part, i.e. converting single byte elements in the range 0x80-0xff to their corresponding CScriptNum elements as I understand it, could be as simple as:
`OP_DUP OP_0 OP_LESSTHAN OP_IF OP_ABS 0x80 OP_ADD OP_ENDIF`
Using that would be slightly easier to reason about I think, as then H_B can indeed be the first non-zero?
If I'm not mistaken, there's also an off-by-one error; if the remainder of the difficulty value is zero (i.e. D % 8 == 0), then all values should be good for H_B I guess (rather than < 128), as the difficulty requirement is already satisfied by the on-the-fly generated all-zeros H_C.
But independently on the script's details, I should really start to write proper tests.
> But hmm, I think even if the difficulty is not hard-coded in the witness script, the signature still commits to the difficulty by the input's nSequence field that needs to be set for OP_CSV.
Ha, quite right. One day I'll be able to keep that in mind, but it's not this day :(
So I guess the idea is that you point your hashpower at whichever utxo is oldest, sign with the maximum CSV that will get it into the next block, and try hashing to satisfy PoW; if that utxo gets spent, you target the next oldest utxo and generate a new signature; and every 10 blocks you generate another new signature with increased CSV and reduced target. You can use a single use private key that's never written to disk for each signature, so you don't have to put meaningful private keys online to do those signatures.
> `OP_DUP OP_0 OP_LESSTHAN OP_IF OP_ABS 0x80 OP_ADD OP_ENDIF`
Yeah, I just assumed negatives were hard and didn't try them. Also, didn't know `OP_ABS` existed! `0x80` is just negative zero, you'd need 0x028000 (push 2 bytes, 0x80 then 0x00) in the notation ParseScript uses.
> So I guess the idea is that you point your hashpower at whichever utxo is oldest, sign with the maximum CSV that will get it into the next block, and try hashing to satisfy PoW; if that utxo gets spent, you target the next oldest utxo and generate a new signature; and every 10 blocks you generate another new signature with increased CSV and reduced target. You can use a single use private key that's never written to disk for each signature, so you don't have to put meaningful private keys online to do those signatures.
Sounds right. Btw, I think it would be quite interesting to also bring in faucaet UTXO's nValue as additional condition on what can be claimed depending on the difficulty (the more PoW, the more sats to claim). But being able to do that seems to be many soft-forks away, [64-bit arithmetics](https://delvingbitcoin.org/t/64-bit-arithmetic-soft-fork/397) probably being one of them. And of course, it would be even harder to stop people from thinking signet coins have value :sweat_smile:
> `0x80` is just negative zero, you'd need 0x028000 (push 2 bytes, 0x80 then 0x00) in the notation ParseScript uses.
Good to know. Being spoiled I just relied on the test framework's `CScript` class here (and the notation looks accordingly) which luckily just takes care of the right minimal-encoded push for bare integers in the list, e.g.:
```
$ python3
...
>>> from test_framework.script import CScript, OP_ABS, OP_ADD
>>> CScript([OP_ABS, 0x80, OP_ADD]).hex()
'9002800093'
```
> Btw, I think it would be quite interesting to also bring in faucaet UTXO's nValue as additional condition on what can be claimed depending on the difficulty
I think you could do that on inquisition now: basically construct a tree of transactions with precommitted APO sigs, so that you've got:
- nvalue = 0.001 BTC -> can only claim everything
- nvalue = 0.002 BTC -> can claim everything with tapscript path A, with path B, have to comply with a `ANYPREVOUTANYSCRIPT | SINGLE` signature that commits to an output with nvalue = 0.001,000,00, allowing you to claim half the funds
- nvalue = 0.003 BTC -> path A = claim everything, path B = APOAS|SINGLE to 0.002, path C = APOAS|SINGLE to 0.001
etc. I don't really think it's worth doing though; if you just have the faucet generate utxos with 1.0, 0.5, 0.25, 0.1, 0.05, 0.025, 0.01, etc each block with the same proof of work script, then the high value ones will get picked off first by miners with high hashpower, letting the lower value ones reduce in difficulty until lower-hashrate miners who are more desperate pick them up. In theory anyway?
Here's my current thought on the script:
```
DUP 0 GREATERTHANOREQUAL VERIFY
DUP 64 LESSTHAN VERIFY
DUP
DUP ADD
DUP DUP ADD
DUP ADD
ADD
640 SWAP SUB
CHECKSEQUENCEVERIFY DROP
FALSE TOALTSTACK
DUP 32 GREATERTHANOREQUAL IF FROMALTSTACK 0x00000000 CAT TOALTSTACK 32 SUB ENDIF
DUP 16 GREATERTHANOREQUAL IF FROMALTSTACK 0x0000 CAT TOALTSTACK 16 SUB ENDIF
DUP 8 GREATERTHANOREQUAL IF FROMALTSTACK 0x00 CAT TOALTSTACK 8 SUB ENDIF
SWAP
SIZE 1 EQUALVERIFY
DUP TOALTSTACK
1 CAT 256 SUB
SWAP
DUP 0 EQUAL IF 256 ELSE
DUP 1 EQUAL IF 128 ELSE
DUP 2 EQUAL IF 64 ELSE
DUP 3 EQUAL IF 32 ELSE
DUP 4 EQUAL IF 16 ELSE
DUP 5 EQUAL IF 8 ELSE
DUP 6 EQUAL IF 4 ELSE
2
ENDIF ENDIF ENDIF ENDIF ENDIF ENDIF ENDIF NIP LESSTHAN VERIFY
FROMALTSTACK FROMALTSTACK CAT CAT TOALTSTACK
// sig pubkey prefix suffix
2OVER
SIZE 32 EQUALVERIFY DROP
SIZE 64 EQUALVERIFY
SWAP CAT CAT HASH256 FROMALTSTACK EQUALVERIFY
CHECKSIG
```
EDIT: I don't entirely like the "prefix + sig + postfix" approach; it lets an attacker select a different portion of the preimage as the signature. Probably impossible to attack, but I also think you can fix that pretty easily: replace `SWAP CAT CAT HASH256 FROMALTSTACK EQUALVERIFY` with
```
SWAP
SIZE SWAP CAT
CAT
SWAP SIZE SWAP CAT SWAP
CAT HASH256 FROMALTSTACK EQUALVERIFY
```
which just puts the sizes in front of them. Then you can make the prefix be 3 bytes, and the suffix be 11 bytes, and it will look a lot like a block header.
> EDIT: I don't entirely like the "prefix + sig + postfix" approach; it lets an attacker select a different portion of the preimage as the signature. Probably impossible to attack,
Ah, not impossible to attack. If you make the preimage be "sig1+sig2+sig3+sig4+nonce" you can do work on multiple coins at once, then use the same pow to claim each coin by changing what the prefix/postfix is for each coin.
How about this: https://github.com/ajtowns/bitcoin/tree/202405-inq27-powcoins
```
$ cd src/
$ make
$ ./src//bitcoind -signet -addnode=inquisition.bitcoin-signet.net -daemon
$ cd contrib/signet
$ ./powcoins setup-wallet
$ ./powcoins --debug claim --max-diff=29 tb1qzv66dscl3uauh5jesfem33wws747naapcgt4c5
DEBUG:root:Calling bitcoin-cli: ['bitcoin-cli', '-signet', 'decodescript', '51205d21df7d162147d4c071d79abfce36a7b160a05ad2d9c21984a0b2173a2e2903']
DEBUG:root:Faucet address tb1pt5sa7lgky9rafsr367dtln3k57ckpgz66tvuyxvy5zepww3w9yps348a9q (delay=10)
DEBUG:root:Calling bitcoin-cli: ['bitcoin-cli', '-signet', 'decodescript', '51201037da469c419050b6d3f1113c2f19ad18dfceaf6b31df9dd13888bc73ae0e58']
DEBUG:root:Faucet address tb1pzqma535ugxg9pdkn7ygnctce45vdln40dvcal8w38zytcuawpevqg75lcj (delay=4)
DEBUG:root:Calling bitcoin-cli: ['bitcoin-cli', '-signet', 'decodescript', '5120b0037812bd4c186640d7ba13587cdfb62f7c0a34948c89296ae8d157c976977c']
DEBUG:root:Faucet address tb1pkqphsy4afsvxvsxhhgf4slxlkchhcz35jjxgj2t2arg40jtkja7qlkv2hl (delay=1)
DEBUG:root:Calling bitcoin-cli: ['bitcoin-cli', '-signet', '-rpcwallet=varpow', 'listunspent']
DEBUG:root:nbits = 01000021 difficulty = 16 csv = 48 invdiff = 48 delay = 1
DEBUG:root:GRIND 0300000041609574014206a229f43cb71635ad3b666525a4a759370d6b471af85b7148c5781196a0abe8ca7266d647a70423a385a65abdae625ab2091871ac24d3f8744c0b0000000100002100000000
DEBUG:root:GROUND 0300000041609574014206a229f43cb71635ad3b666525a4a759370d6b471af85b7148c5781196a0abe8ca7266d647a70423a385a65abdae625ab2091871ac24d3f8744c0b0000000100002146fa0000
DEBUG:root:tx 02000000000101f9de43e6712e7194d89bfd2847c3d967275d9b2106fa456a172e1fd07b6cae96030000000030000000011095be00000000001600141335a6c31f8f3bcbd2598273b8c5ce87abe9f7a1094041609574014206a229f43cb71635ad3b666525a4a759370d6b471af85b7148c5781196a0abe8ca7266d647a70423a385a65abdae625ab2091871ac24d3f8744c2079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798030000000b0000000100002146fa00001d4fa2d7ce8d2836e796e0b30af29ddb5c3064b3468614914f068c522fa5012b0110a27651a2697601409f697601407c94b275006b760120a2636c04000000007e6b012094687660a2636c0200007e6b6094687658a2636c01007e6b5894687c825188766b517e020001947c7600876302000167765187630280006776528763014067765387630120677654876360677655876358677656876354675268686868686868779f696c6c7e7e6b708201208875820140887c827c7e7e7c827c7e7c7eaa6c88ac21c04a981eda2acd9bf5d789fa1a9b7f95677776d99fd13ec0f658f0e4dbc5b5c87800000000
DEBUG:root:Calling bitcoin-cli: ['bitcoin-cli', '-signet', 'sendrawtransaction', '02000000000101f9de43e6712e7194d89bfd2847c3d967275d9b2106fa456a172e1fd07b6cae96030000000030000000011095be00000000001600141335a6c31f8f3bcbd2598273b8c5ce87abe9f7a1094041609574014206a229f43cb71635ad3b666525a4a759370d6b471af85b7148c5781196a0abe8ca7266d647a70423a385a65abdae625ab2091871ac24d3f8744c2079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798030000000b0000000100002146fa00001d4fa2d7ce8d2836e796e0b30af29ddb5c3064b3468614914f068c522fa5012b0110a27651a2697601409f697601407c94b275006b760120a2636c04000000007e6b012094687660a2636c0200007e6b6094687658a2636c01007e6b5894687c825188766b517e020001947c7600876302000167765187630280006776528763014067765387630120677654876360677655876358677656876354675268686868686868779f696c6c7e7e6b708201208875820140887c827c7e7e7c827c7e7c7eaa6c88ac21c04a981eda2acd9bf5d789fa1a9b7f95677776d99fd13ec0f658f0e4dbc5b5c87800000000']
INFO:root:sendrawtransaction result 0193ae789de9f3426521e3cc2a4698e0d9f5a5351dfcb95c018cf7817029b584
```
which is then pushed over the network and [eventually mined](https://mempool.space/signet/tx/0193ae789de9f3426521e3cc2a4698e0d9f5a5351dfcb95c018cf7817029b584)
I did three different scripts -- one that halves the difficulty every block, one every 4 blocks, and one every 10 blocks; and am sending 0.125 sBTC, 1 sBTC and 2 sBTC to them respectively each block.
It's using `bitcoin-util grind` to solve the nonce, setting the fake block header's prevhash and merkleroot to the signature, and setting nBits to match the desired difficulty, so I think in theory you could replace that with an interface to a mining pool.
I think I did the math right so that it'll always go for whichever coin gives you the most value for your hashpower.
Neat! I haven't had a chance yet to look closer at the latest proposal of the script, but using that powcoins tool from your branch worked flawlessly with `--max-diff=29`: https://mempool.space/signet/tx/95e85e4a2509ee01af974f0576717a4616d940f781764e13ef370adbded62737
As a small side-detail, in a "real-world scenario" (if that even makes sense) I think the internal pubkey should be provably unspendable, to not disincentivize claimers from doing PoW? (maybe it already is, but at least `ipk` doesn't seem to be NUMS_H)
> Neat! I haven't had a chance yet to look closer at the latest proposal of the script, but using that powcoins tool from your branch worked flawlessly with `--max-diff=29`: https://mempool.space/signet/tx/95e85e4a2509ee01af974f0576717a4616d940f781764e13ef370adbded62737 As a small side-detail, in a "real-world scenario" (if that even makes sense) I think the internal pubkey should be provably unspendable, to not disincentivize claimers from doing PoW? (maybe it already is, but at least `ipk` doesn't seem to be NUMS_H)
I set ipk to a pubkey from my signet wallet, so that in theory if I messed up the script I could reclaim the funds. Switching to `NUMS_H` seems like a good idea once it's a bit more tested.
After talking with @kallewoof I've changed the difficulty range from 1-64 to 16-80; 80 is about 220k times more difficult than finding a block on mainnet, so should be good for a while :)
> One thing that might be interesting: if you construct the data being hashed as "X SIG Y", and used "HASH256", people could make X be 4 bytes, and Y be 12 bytes, and probably would then be able to use regular mining software/hardware to grind an answer,
Looking into [stratumv1 code](https://github.com/skot/ESP-Miner/blob/master/components/stratum/mining.c), it seems like this was a bit too simplistic, and you actually want to provide a coinbase tx and so on; so I think a better construction would be:
* version genesis-hash merkle-root timestamp nbits nonce (80 byte header, hash of this must meet proof of work target)
* merkle-root = sha256d( 32byte "coinbase" hash, sha256( sig ) )
So the data you provide is:
* pubkey, sig (96 bytes)
* prefix = version, genesis-hash (36 bytes)
* suffix = timestamp nbits nonce (12 bytes)
* cbhash = "coinbase" hash (32 bytes)
You could perhaps use G/2 as the pubkey and move its zero-bytes to the end and reuse it as a fake block hash to save ~26 bytes while still looking pretty legit (ie `"0000000000000000000000" "3b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63" 2DUP SWAP CAT TOALT CAT` or so).