Skip to content

Commit 89994c5

Browse files
feat: add btc-timestamp-file command (#24)
* feat: add btc-timestamp-file command * chore: rename bzPkTapRoot to taprootPkScript * chore: use PayToTaprootScript and add flag to tx fee * feat: add command crate-timestamp-account * chore: refactory timestamping to avoid code duplication * feat: add command to create-timestamp-account and create-timestamp-transaction * chore: adding test to check btc timestamp file, currently failing on estimate fee on FundRawTx * chore: add WalletPassphrase before sign * test: send funded signed raw tx * feat: add fee calc * test: sendrawtx of timestamp tx given mandatory-script-verify-flag-failed (Witness program was passed an empty witness) * use p2wpkh address as change output * chore: remove CreateTimestampAcc and fix lint * chore: test of btc timestamp file * chore: update comment and descriptions * chore: add doc for how to timestamp file * fix: type of flag FlagFeeInTx * fix: misspell pubkey and P2WPKH * chore: removed unused structure * chore: add check for value in to be bigger than fee * chore: add test that timestamp a file from the funded timestamped transaction * chore: address comments on PR for doc --------- Co-authored-by: KonradStaniec <[email protected]>
1 parent bf17e4a commit 89994c5

File tree

8 files changed

+565
-24
lines changed

8 files changed

+565
-24
lines changed

cmd/createStakingTxCmd.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func serializeBTCTx(tx *wire.MsgTx) ([]byte, error) {
7272
return txBuf.Bytes(), nil
7373
}
7474

75-
func serializeBTCTxToHex(tx *wire.MsgTx) (string, error) {
75+
func SerializeBTCTxToHex(tx *wire.MsgTx) (string, error) {
7676
bytes, err := serializeBTCTx(tx)
7777

7878
if err != nil {
@@ -302,7 +302,7 @@ var createStakingTxCmd = &cobra.Command{
302302
return err
303303
}
304304

305-
serializedTx, err := serializeBTCTxToHex(tx)
305+
serializedTx, err := SerializeBTCTxToHex(tx)
306306
if err != nil {
307307
return err
308308
}

cmd/createUnbondingTxCmd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ var createUnbondingTxCmd = &cobra.Command{
225225

226226
unbondingTxHash := unbondingTx.TxHash()
227227

228-
unbondingTxHex, err := serializeBTCTxToHex(unbondingTx)
228+
unbondingTxHex, err := SerializeBTCTxToHex(unbondingTx)
229229

230230
if err != nil {
231231
return err

cmd/createWithdrawTxCmg.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ var createWithdrawCmd = &cobra.Command{
254254
}
255255

256256
// at this point we created unsigned withdraw tx lets create response
257-
serializedWithdrawTx, err := serializeBTCTxToHex(info.spendStakeTx)
257+
serializedWithdrawTx, err := SerializeBTCTxToHex(info.spendStakeTx)
258258

259259
if err != nil {
260260
return err
@@ -348,7 +348,7 @@ var createWithdrawCmd = &cobra.Command{
348348

349349
// serialize tx with witness
350350

351-
serializedWithdrawTx, err = serializeBTCTxToHex(info.spendStakeTx)
351+
serializedWithdrawTx, err = SerializeBTCTxToHex(info.spendStakeTx)
352352

353353
if err != nil {
354354
return err

cmd/timestampFileCmd.go

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"crypto/sha256"
6+
"encoding/hex"
7+
"fmt"
8+
"io"
9+
"os"
10+
11+
"github.com/btcsuite/btcd/btcutil"
12+
"github.com/btcsuite/btcd/chaincfg"
13+
"github.com/btcsuite/btcd/txscript"
14+
"github.com/btcsuite/btcd/wire"
15+
"github.com/spf13/cobra"
16+
)
17+
18+
const (
19+
FlagFeeInTx = "fee-in-tx"
20+
)
21+
22+
type TimestampFileOutput struct {
23+
TimestampTx string `json:"timestamp_tx_hex"`
24+
FileHash string `json:"file_hash"`
25+
}
26+
27+
func init() {
28+
_ = btcTimestampFileCmd.Flags().Int64(FlagFeeInTx, 2000, "the amount of satoshi to pay as fee for the tx")
29+
_ = btcTimestampFileCmd.Flags().String(FlagNetwork, "signet", "network one of (mainnet, testnet3, regtest, simnet, signet)")
30+
31+
rootCmd.AddCommand(btcTimestampFileCmd)
32+
}
33+
34+
var btcTimestampFileCmd = &cobra.Command{
35+
Use: "create-timestamp-transaction [funded-tx-addr-hex] [file-path] [address]",
36+
Example: `cli-tools create-timestamp-transaction [funded-tx-addr-hex] ./path/to/file/to/timestamp 836e9fc730ff37de48f2ff3a76b3c2380fbabaf66d9e50754d86b2a2e2952156`,
37+
Short: "Creates a timestamp btc transaction by hashing the file input.",
38+
Long: `Creates a timestamp BTC transaction with 2 outputs and one input.
39+
One output is the nullDataScript of the file hash, as the file hash
40+
being the sha256 of the input file path. This output is the timestamp of the file.
41+
The other output is the pay to addr script which contains the pay to witness pubkey
42+
with the value as ({funded-tx-output-value} - {FlagFeeInTx}). This output is needed
43+
to continue to have spendable funds to the p2wpkh address.`,
44+
Args: cobra.ExactArgs(3),
45+
RunE: func(cmd *cobra.Command, args []string) error {
46+
fundedTxHex, inputFilePath, addressStr := args[0], args[1], args[2]
47+
flags := cmd.Flags()
48+
feeInTx, err := flags.GetInt64(FlagFeeInTx)
49+
if err != nil {
50+
return fmt.Errorf("failed to parse flag %s: %w", FlagFeeInTx, err)
51+
}
52+
53+
networkParamStr, err := flags.GetString(FlagNetwork)
54+
if err != nil {
55+
return fmt.Errorf("failed to parse flag %s: %w", FlagNetwork, err)
56+
}
57+
58+
btcParams, err := getBtcNetworkParams(networkParamStr)
59+
if err != nil {
60+
return fmt.Errorf("unable parse BTC network %s: %w", networkParamStr, err)
61+
}
62+
63+
timestampOutput, err := CreateTimestampTx(fundedTxHex, inputFilePath, addressStr, feeInTx, btcParams)
64+
if err != nil {
65+
return fmt.Errorf("failed to create timestamping tx: %w", err)
66+
}
67+
68+
PrintRespJSON(timestampOutput)
69+
return nil
70+
},
71+
}
72+
73+
func outputIndexForPkScript(pkScript []byte, tx *wire.MsgTx) (int, error) {
74+
for i, txOut := range tx.TxOut {
75+
if bytes.Equal(txOut.PkScript, pkScript) {
76+
return i, nil
77+
}
78+
}
79+
return -1, fmt.Errorf("unable to find output index for pk script")
80+
}
81+
82+
// CreateTimestampTx outputs the hash of file and BTC transaction that timestamp that
83+
// hash in one of the outputs. The funded tx needs to have one output with value
84+
// for the changeAddress p2wpkh. The changeAddress needs to be a EncodeAddress
85+
// which is the encoding of the payment address associated with the Address value,
86+
// to be used to generate a pay to address script pay-to-witness-pubkey-hash (P2WKH) format.
87+
func CreateTimestampTx(
88+
fundedTxHex, filePath, changeAddress string,
89+
fee int64,
90+
networkParams *chaincfg.Params,
91+
) (*TimestampFileOutput, error) {
92+
txOutFileHash, fileHash, err := txOutTimestampFile(filePath)
93+
if err != nil {
94+
return nil, fmt.Errorf("unable to create tx out with filepath %s: %w", filePath, err)
95+
}
96+
97+
fundingTx, _, err := newBTCTxFromHex(fundedTxHex)
98+
if err != nil {
99+
return nil, fmt.Errorf("unable parse BTC Tx %s: %w", fundedTxHex, err)
100+
}
101+
102+
address, err := btcutil.DecodeAddress(changeAddress, networkParams)
103+
if err != nil {
104+
return nil, fmt.Errorf("invalid address %s: %w", changeAddress, err)
105+
}
106+
107+
addressPkScript, err := txscript.PayToAddrScript(address)
108+
if err != nil {
109+
return nil, fmt.Errorf("unable to create pk script from address %s: %w", changeAddress, err)
110+
}
111+
112+
if !txscript.IsPayToWitnessPubKeyHash(addressPkScript) {
113+
return nil, fmt.Errorf("address %s is not a pay-to-witness-pubkey-hash", changeAddress)
114+
}
115+
116+
fundingOutputIdx, err := outputIndexForPkScript(addressPkScript, fundingTx)
117+
if err != nil {
118+
return nil, fmt.Errorf("unable to find output index for pk script: %w", err)
119+
}
120+
fundingTxHash := fundingTx.TxHash()
121+
fundingInput := wire.NewTxIn(
122+
wire.NewOutPoint(&fundingTxHash, uint32(fundingOutputIdx)),
123+
nil,
124+
nil,
125+
)
126+
127+
valueIn := fundingTx.TxOut[fundingOutputIdx].Value
128+
if valueIn < fee {
129+
return nil, fmt.Errorf("the value of input in %d is bigger than the fee %d", valueIn, fee)
130+
}
131+
132+
changeOutput := wire.NewTxOut(
133+
valueIn-fee,
134+
addressPkScript,
135+
)
136+
137+
timestampTx := wire.NewMsgTx(2)
138+
timestampTx.AddTxIn(fundingInput)
139+
timestampTx.AddTxOut(changeOutput)
140+
timestampTx.AddTxOut(txOutFileHash)
141+
142+
txHex, err := SerializeBTCTxToHex(timestampTx)
143+
if err != nil {
144+
return nil, fmt.Errorf("failed to serialize timestamping tx: %w", err)
145+
}
146+
147+
return &TimestampFileOutput{
148+
TimestampTx: txHex,
149+
FileHash: hex.EncodeToString(fileHash),
150+
}, nil
151+
}
152+
153+
func txOutTimestampFile(filePath string) (txOut *wire.TxOut, fileHash []byte, err error) {
154+
fileHash, err = hashFromFile(filePath)
155+
if err != nil {
156+
return nil, nil, fmt.Errorf("failed to generate hash from file %s: %w", filePath, err)
157+
}
158+
159+
dataScript, err := txscript.NullDataScript(fileHash)
160+
if err != nil {
161+
return nil, nil, fmt.Errorf("failed to create op return with hash from file %s: %w", fileHash, err)
162+
}
163+
164+
return wire.NewTxOut(0, dataScript), fileHash, nil
165+
}
166+
167+
func hashFromFile(filePath string) ([]byte, error) {
168+
h := sha256.New()
169+
170+
f, err := os.Open(filePath)
171+
if err != nil {
172+
return nil, fmt.Errorf("failed to open the file %s: %w", filePath, err)
173+
}
174+
defer f.Close()
175+
176+
if _, err := io.Copy(h, f); err != nil {
177+
return nil, err
178+
}
179+
180+
return h.Sum(nil), nil
181+
}

docs/commands.md

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Explain CLI commands
2+
3+
## BTC Timestamp File
4+
5+
The command `create-timestamp-transaction` is used to timestamp a file into bitcoin.
6+
In this case, timestamping refers to generating a SHA256 hash of the file and
7+
creating an output containing a `NullDataScript` with the hash in it and a zero
8+
value. This transaction needs a funded encoded address that converts pubkeys to
9+
P2WPKH addresses.
10+
11+
Following are instructions on how the timestamp can be generated and submitted
12+
to Bitcoin. To execute the below commands, you need a running
13+
[bitcoind](https://github.com/bitcoin/bitcoin) daemon that has access to a
14+
funded wallet.
15+
16+
1. Create a new Wallet
17+
18+
```shell
19+
$ bitcoin-cli createwallet timestamp-w
20+
21+
{
22+
"name": "timestamp-w"
23+
}
24+
```
25+
26+
2. Generate a new address
27+
28+
```shell
29+
$ bitcoin-cli -rpcwallet=timestamp-w getnewaddress
30+
31+
bcrt1qagw463xxngygwsdjrm5f4etvr2hkq0yq0up8ly
32+
```
33+
34+
3. Send funds to this address
35+
36+
```shell
37+
$ bitcoin-cli -rpcwallet=wallet-with-funds sendtoaddress bcrt1qagw463xxngygwsdjrm5f4etvr2hkq0yq0up8ly 15
38+
39+
6ce4a2b2797da995d2f49ffd707d1bb34f1717205e6f7fa3d6f3f1aa8b2c9eca
40+
```
41+
42+
4. Get the tx data of the `sendtoaddress` transaction
43+
44+
```shell
45+
$ bitcoin-cli -rpcwallet=timestamp-w getrawtransaction 6ce4a2b2797da995d2f49ffd707d1bb34f1717205e6f7fa3d6f3f1aa8b2c9eca
46+
47+
020000000001014db94bb61981724775d3a9a27dc62facbb422e5d76887205019dab39e031f7400000000000fdffffff02002f685900000000160014ea1d5d44c69a088741b21ee89ae56c1aaf603c80389c9bd000000000160014dd619252c3983779e6938366ece5d4e2015172b3024730440220180472b948c4fc4e2c608462375b41ff545ef2d0fe97d3ae8ac1e6ad25a6de1f0220036e23c181cb4b65264a58524215e97b27501d1039cd3a2f7d009f860aa8ed46012102a932128cbf3ddf90e12a4c94a6acce81ea3d1ea1dc325101f90b013ccecc60fe95000000
48+
```
49+
50+
5. Create the timestamp transaction using the `create-timestamp-transaction` utility.
51+
The first parameter corresponds to the transaction data, the second is the file
52+
that will be timestamped, and the last parameter is the address previously
53+
created that will fund the transaction.
54+
55+
```shell
56+
$ ./build/cli-tools create-timestamp-transaction 020000000001014db94bb61981724775d3a9a27dc62facbb422e5d76887205019dab39e031f7400000000000fdffffff02002f685900000000160014ea1d5d44c69a088741b21ee89ae56c1aaf603c80389c9bd000000000160014dd619252c3983779e6938366ece5d4e2015172b3024730440220180472b948c4fc4e2c608462375b41ff545ef2d0fe97d3ae8ac1e6ad25a6de1f0220036e23c181cb4b65264a58524215e97b27501d1039cd3a2f7d009f860aa8ed46012102a932128cbf3ddf90e12a4c94a6acce81ea3d1ea1dc325101f90b013ccecc60fe95000000 ./README.md bcrt1qagw463xxngygwsdjrm5f4etvr2hkq0yq0up8ly
57+
58+
{
59+
"timestamp_tx_hex": "0200000001ca9e2c8baaf1f3d6a37f6f5e2017174fb31b7d70fd9ff4d295a97d79b2a2e46c0000000000ffffffff023027685900000000160014ea1d5d44c69a088741b21ee89ae56c1aaf603c800000000000000000226a203531589f3a9715ed9a130aa2702a17d8251f767e5412447ecbcfedfef5b9798f00000000",
60+
"file_hash": "3531589f3a9715ed9a130aa2702a17d8251f767e5412447ecbcfedfef5b9798f"
61+
}
62+
```
63+
64+
6. Sign the transaction by passing the `timestamp_tx_hex` property of the
65+
`create-timestamp-transaction` command.
66+
67+
```shell
68+
$ bitcoin-cli -rpcwallet=timestamp-w signrawtransactionwithwallet 0200000001ca9e2c8baaf1f3d6a37f6f5e2017174fb31b7d70fd9ff4d295a97d79b2a2e46c0000000000ffffffff023027685900000000160014ea1d5d44c69a088741b21ee89ae56c1aaf603c800000000000000000226a203531589f3a9715ed9a130aa2702a17d8251f767e5412447ecbcfedfef5b9798f00000000
69+
70+
{
71+
"hex": "02000000000101ca9e2c8baaf1f3d6a37f6f5e2017174fb31b7d70fd9ff4d295a97d79b2a2e46c0000000000ffffffff023027685900000000160014ea1d5d44c69a088741b21ee89ae56c1aaf603c800000000000000000226a203531589f3a9715ed9a130aa2702a17d8251f767e5412447ecbcfedfef5b9798f0247304402206f272dcc7b94474dd6df3f0d4eafd5e96ef7e568ec391ca9e06b466cdb694677022039546f7fc09a955d2b71ba02b927a839b91865c72d04d811e2a0f2f0838030ef0121029928fe0c0b89122500dee8a6b29cdac54925770f0d484778ff2be878854e1c4a00000000",
72+
"complete": true
73+
}
74+
```
75+
76+
7. Broadcast the transaction to Bitcoin
77+
78+
```shell
79+
$ bitcoin-cli -rpcwallet=timestamp-w sendrawtransaction 02000000000101ca9e2c8baaf1f3d6a37f6f5e2017174fb31b7d70fd9ff4d295a97d79b2a2e46c0000000000ffffffff023027685900000000160014ea1d5d44c69a088741b21ee89ae56c1aaf603c800000000000000000226a203531589f3a9715ed9a130aa2702a17d8251f767e5412447ecbcfedfef5b9798f0247304402206f272dcc7b94474dd6df3f0d4eafd5e96ef7e568ec391ca9e06b466cdb694677022039546f7fc09a955d2b71ba02b927a839b91865c72d04d811e2a0f2f0838030ef0121029928fe0c0b89122500dee8a6b29cdac54925770f0d484778ff2be878854e1c4a00000000
80+
81+
25b65b31c6f4d2f46ebeb5fa4c9a6d1fa4de03813404718f9797269b79023122
82+
```
83+
84+
8. To verify whether the transactions has been included, you can query Bitcoin
85+
and check the transaction's number of confirmations.
86+
87+
```shell
88+
$ bitcoin-cli -rpcwallet=timestamp-w gettransaction 25b65b31c6f4d2f46ebeb5fa4c9a6d1fa4de03813404718f9797269b79023122
89+
90+
{
91+
"amount": 0.00000000,
92+
"fee": -0.00002000,
93+
"confirmations": 4,
94+
"blockhash": "46582163473053a7e5f8743a79675d9f9cc5cbf4890f28f1e7e816316b28ef69",
95+
"blockheight": 201,
96+
"blockindex": 2,
97+
"blocktime": 1715821032,
98+
"txid": "25b65b31c6f4d2f46ebeb5fa4c9a6d1fa4de03813404718f9797269b79023122",
99+
"wtxid": "3b2ce841ef57b43bc5b59d772ccc6fcaf38ca2bce94dbb787a5791a4ee0765df",
100+
"walletconflicts": [
101+
],
102+
"time": 1715820976,
103+
"timereceived": 1715820976,
104+
"bip125-replaceable": "no",
105+
"details": [
106+
{
107+
"address": "bcrt1qagw463xxngygwsdjrm5f4etvr2hkq0yq0up8ly",
108+
"category": "send",
109+
"amount": -14.99998000,
110+
"label": "",
111+
"vout": 0,
112+
"fee": -0.00002000,
113+
"abandoned": false
114+
},
115+
{
116+
"category": "send",
117+
"amount": 0.00000000,
118+
"vout": 1,
119+
"fee": -0.00002000,
120+
"abandoned": false
121+
},
122+
{
123+
"address": "bcrt1qagw463xxngygwsdjrm5f4etvr2hkq0yq0up8ly",
124+
"parent_descs": [
125+
"wpkh(tpubD6NzVbkrYhZ4X4XfxiyNepqE9Uy48atSoXCcd68V1hNokNUFiVUF6AptTPPDcU2mnLumM3m5Jq3eLjaoNd2vQdDtNjhGyfU2HJVSBEPYdtD/84h/1h/0h/0/*)#fawa8eh6"
126+
],
127+
"category": "receive",
128+
"amount": 14.99998000,
129+
"label": "",
130+
"vout": 0,
131+
"abandoned": false
132+
}
133+
],
134+
"hex": "02000000000101ca9e2c8baaf1f3d6a37f6f5e2017174fb31b7d70fd9ff4d295a97d79b2a2e46c0000000000ffffffff023027685900000000160014ea1d5d44c69a088741b21ee89ae56c1aaf603c800000000000000000226a203531589f3a9715ed9a130aa2702a17d8251f767e5412447ecbcfedfef5b9798f0247304402206f272dcc7b94474dd6df3f0d4eafd5e96ef7e568ec391ca9e06b466cdb694677022039546f7fc09a955d2b71ba02b927a839b91865c72d04d811e2a0f2f0838030ef0121029928fe0c0b89122500dee8a6b29cdac54925770f0d484778ff2be878854e1c4a00000000",
135+
"lastprocessedblock": {
136+
"hash": "27bc75d8210c69b72f0304aecb8c72c109691b8f94dd7d022ae5fc9026fb4008",
137+
"height": 204
138+
}
139+
}
140+
```

0 commit comments

Comments
 (0)