|
| 1 | +package universe |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + "context" |
| 6 | + "crypto/tls" |
| 7 | + "encoding/hex" |
| 8 | + "fmt" |
| 9 | + "net" |
| 10 | + "os" |
| 11 | + "testing" |
| 12 | + "time" |
| 13 | + |
| 14 | + "github.com/btcsuite/btcd/btcec/v2" |
| 15 | + "github.com/btcsuite/btcd/btcec/v2/schnorr" |
| 16 | + "github.com/btcsuite/btcd/chaincfg" |
| 17 | + "github.com/btcsuite/btcd/wire" |
| 18 | + "github.com/btcsuite/btcwallet/chain" |
| 19 | + "github.com/lightninglabs/taproot-assets/asset" |
| 20 | + "github.com/lightninglabs/taproot-assets/proof" |
| 21 | + unirpc "github.com/lightninglabs/taproot-assets/taprpc/universerpc" |
| 22 | + "github.com/lightningnetwork/lnd/lncfg" |
| 23 | + "github.com/lightningnetwork/lnd/tor" |
| 24 | + "github.com/stretchr/testify/require" |
| 25 | + "google.golang.org/grpc" |
| 26 | + "google.golang.org/grpc/credentials" |
| 27 | +) |
| 28 | + |
| 29 | +// TestStitchProofsForDebugging is a test that can be used to fetch a partial |
| 30 | +// starting proof file from a universe and append/stitch together additional |
| 31 | +// proofs for debugging purposes. It is not meant to be run as part of the |
| 32 | +// regular test suite, but can be used to debug issues locally or to manually |
| 33 | +// fix proofs that failed for some reason. |
| 34 | +// A potential workflow to fix failed proofs could look like this: |
| 35 | +// - Copy the new_proof_blob field from a failed transfer output into the |
| 36 | +// proof/testdata/proof.hex file. |
| 37 | +// - Run the TestProofVerification test to see what's wrong, manually fix what |
| 38 | +// needs to be fixed, then re-encode the proof and get the raw hex. |
| 39 | +// - Edit the outpoint/groupKeyBytes/assetIDBytes/scriptKeyBytes below and set |
| 40 | +// them to the last known proof in the universe that is right before the |
| 41 | +// failed proof. |
| 42 | +// - Find out in which block the transaction for the failed proof was included |
| 43 | +// in and then set the stitchMap to the block height and the raw hex string |
| 44 | +// of the manually fixed proof (or multiple proofs). |
| 45 | +// - Run the test and import the resulting proof file into the node. |
| 46 | +func TestFetchProofFromUniverseForDebugging(t *testing.T) { |
| 47 | + // Comment this out for local debugging. |
| 48 | + t.Skipf("This test is for debugging purposes only.") |
| 49 | + |
| 50 | + // EDIT the following constants and variables: |
| 51 | + const ( |
| 52 | + universeServer = "universe.lightning.finance:10029" |
| 53 | + bitcoindServer = "localhost:8332" |
| 54 | + bitcoindUser = "lightning" |
| 55 | + bitcoindPass = "lightning" |
| 56 | + ) |
| 57 | + var ( |
| 58 | + outpoint, _ = wire.NewOutPointFromString( |
| 59 | + "xxxx:0", |
| 60 | + ) |
| 61 | + groupKeyBytes, _ = hex.DecodeString( |
| 62 | + "02xxxx", |
| 63 | + ) |
| 64 | + assetIDBytes, _ = hex.DecodeString( |
| 65 | + "xxxxx", |
| 66 | + ) |
| 67 | + scriptKeyBytes, _ = hex.DecodeString( |
| 68 | + "02xxxx", |
| 69 | + ) |
| 70 | + // stitchMap is a map of block heights to the raw proof as a hex |
| 71 | + // dump that should be stitched into the proof file. We assume |
| 72 | + // that the proofs come from the output of a partial transfer |
| 73 | + // (field new_proof_blob on the "tapcli assets transfers" |
| 74 | + // output), where the proofs don't have a block height/header |
| 75 | + // set yet. Assuming the transaction already confirmed, we will |
| 76 | + // set the block height/header and stitch the proof into the |
| 77 | + // full file. |
| 78 | + stitchMap = map[int64]string{ |
| 79 | + 900115: "544150500004000000xxxxxxxxxxxxxxx", |
| 80 | + 900116: "544150500004000000xxxxxxxxxxxxxxx", |
| 81 | + } |
| 82 | + ) |
| 83 | + |
| 84 | + ctx := context.Background() |
| 85 | + tlsConfig := tls.Config{InsecureSkipVerify: true} |
| 86 | + transportCredentials := credentials.NewTLS(&tlsConfig) |
| 87 | + |
| 88 | + clientConn, err := grpc.NewClient( |
| 89 | + universeServer, |
| 90 | + grpc.WithTransportCredentials(transportCredentials), |
| 91 | + ) |
| 92 | + require.NoError(t, err) |
| 93 | + |
| 94 | + t.Cleanup(func() { |
| 95 | + err := clientConn.Close() |
| 96 | + require.NoError(t, err) |
| 97 | + }) |
| 98 | + |
| 99 | + src := unirpc.NewUniverseClient(clientConn) |
| 100 | + fetchUniProof := func(ctx context.Context, |
| 101 | + loc proof.Locator) (proof.Blob, error) { |
| 102 | + |
| 103 | + uniID := Identifier{ |
| 104 | + AssetID: *loc.AssetID, |
| 105 | + } |
| 106 | + if loc.GroupKey != nil { |
| 107 | + uniID.GroupKey = loc.GroupKey |
| 108 | + } |
| 109 | + |
| 110 | + rpcUniID, err := marshalUniID(uniID) |
| 111 | + require.NoError(t, err) |
| 112 | + |
| 113 | + op := &unirpc.Outpoint{ |
| 114 | + HashStr: loc.OutPoint.Hash.String(), |
| 115 | + Index: int32(loc.OutPoint.Index), |
| 116 | + } |
| 117 | + scriptKeyBytes := loc.ScriptKey.SerializeCompressed() |
| 118 | + |
| 119 | + uniProof, err := src.QueryProof(ctx, &unirpc.UniverseKey{ |
| 120 | + Id: rpcUniID, |
| 121 | + LeafKey: &unirpc.AssetKey{ |
| 122 | + Outpoint: &unirpc.AssetKey_Op{ |
| 123 | + Op: op, |
| 124 | + }, |
| 125 | + ScriptKey: &unirpc.AssetKey_ScriptKeyBytes{ |
| 126 | + ScriptKeyBytes: scriptKeyBytes, |
| 127 | + }, |
| 128 | + }, |
| 129 | + }) |
| 130 | + if err != nil { |
| 131 | + return nil, err |
| 132 | + } |
| 133 | + |
| 134 | + return uniProof.AssetLeaf.Proof, nil |
| 135 | + } |
| 136 | + |
| 137 | + var ( |
| 138 | + assetID *asset.ID |
| 139 | + groupKey *btcec.PublicKey |
| 140 | + scriptPubKey *btcec.PublicKey |
| 141 | + ) |
| 142 | + |
| 143 | + if len(groupKeyBytes) > 0 { |
| 144 | + groupKey, err = btcec.ParsePubKey(groupKeyBytes) |
| 145 | + require.NoError(t, err) |
| 146 | + } |
| 147 | + if len(assetIDBytes) > 0 { |
| 148 | + assetID = new(asset.ID) |
| 149 | + copy(assetID[:], assetIDBytes) |
| 150 | + } |
| 151 | + |
| 152 | + scriptPubKey, err = btcec.ParsePubKey(scriptKeyBytes) |
| 153 | + require.NoError(t, err) |
| 154 | + |
| 155 | + locator := proof.Locator{ |
| 156 | + OutPoint: outpoint, |
| 157 | + AssetID: assetID, |
| 158 | + GroupKey: groupKey, |
| 159 | + ScriptKey: *scriptPubKey, |
| 160 | + } |
| 161 | + |
| 162 | + fullFile, err := proof.FetchProofProvenance( |
| 163 | + ctx, nil, locator, fetchUniProof, |
| 164 | + ) |
| 165 | + require.NoError(t, err) |
| 166 | + |
| 167 | + for i := uint32(0); i < uint32(fullFile.NumProofs()); i++ { |
| 168 | + p, err := fullFile.ProofAt(i) |
| 169 | + require.NoError(t, err) |
| 170 | + |
| 171 | + // EDIT this or comment out according to your needs. In this |
| 172 | + // specific case, the proofs were from a channel commitment and |
| 173 | + // sweep transaction, which didn't use V1 proofs yet. So we |
| 174 | + // needed to manually remove the STXO proofs to allow them to |
| 175 | + // be validated. |
| 176 | + p.InclusionProof.CommitmentProof.STXOProofs = nil |
| 177 | + for idx := range p.ExclusionProofs { |
| 178 | + if p.ExclusionProofs[idx].CommitmentProof == nil { |
| 179 | + continue |
| 180 | + } |
| 181 | + |
| 182 | + p.ExclusionProofs[idx].CommitmentProof.STXOProofs = nil |
| 183 | + } |
| 184 | + |
| 185 | + err = fullFile.ReplaceProofAt(i, *p) |
| 186 | + require.NoError(t, err) |
| 187 | + } |
| 188 | + |
| 189 | + bitcoindCfg := &chain.BitcoindConfig{ |
| 190 | + ChainParams: &chaincfg.MainNetParams, |
| 191 | + Host: bitcoindServer, |
| 192 | + User: bitcoindUser, |
| 193 | + Pass: bitcoindPass, |
| 194 | + Dialer: func(s string) (net.Conn, error) { |
| 195 | + dialer := &tor.ClearNet{} |
| 196 | + return dialer.Dial("tcp", s, time.Minute) |
| 197 | + }, |
| 198 | + PrunedModeMaxPeers: 10, |
| 199 | + PollingConfig: &chain.PollingConfig{ |
| 200 | + BlockPollingInterval: time.Minute, |
| 201 | + TxPollingInterval: time.Minute, |
| 202 | + TxPollingIntervalJitter: lncfg.DefaultTxPollingJitter, |
| 203 | + }, |
| 204 | + } |
| 205 | + |
| 206 | + // Establish the connection to bitcoind and create the clients |
| 207 | + // required for our relevant subsystems. |
| 208 | + bitcoindConn, err := chain.NewBitcoindConn(bitcoindCfg) |
| 209 | + require.NoError(t, err) |
| 210 | + client := bitcoindConn.NewBitcoindClient() |
| 211 | + |
| 212 | + for blockHeight, proofHex := range stitchMap { |
| 213 | + proofBytes, err := hex.DecodeString(proofHex) |
| 214 | + require.NoError(t, err) |
| 215 | + |
| 216 | + stitchProof, err := proof.Decode(proofBytes) |
| 217 | + require.NoError(t, err) |
| 218 | + stitchProof.Version = 0 |
| 219 | + |
| 220 | + blockHash, err := client.GetBlockHash(blockHeight) |
| 221 | + require.NoError(t, err) |
| 222 | + |
| 223 | + block, err := client.GetBlock(blockHash) |
| 224 | + require.NoError(t, err) |
| 225 | + |
| 226 | + stitchProof.BlockHeight = uint32(blockHeight) |
| 227 | + stitchProof.BlockHeader = block.Header |
| 228 | + |
| 229 | + idx := -1 |
| 230 | + for i, tx := range block.Transactions { |
| 231 | + if tx.TxHash() == stitchProof.OutPoint().Hash { |
| 232 | + idx = i |
| 233 | + break |
| 234 | + } |
| 235 | + } |
| 236 | + require.GreaterOrEqual(t, idx, 0, "tx not found in block") |
| 237 | + |
| 238 | + merkleProof, err := proof.NewTxMerkleProof( |
| 239 | + block.Transactions, idx, |
| 240 | + ) |
| 241 | + require.NoError(t, err) |
| 242 | + |
| 243 | + stitchProof.TxMerkleProof = *merkleProof |
| 244 | + |
| 245 | + err = fullFile.AppendProof(*stitchProof) |
| 246 | + require.NoError(t, err) |
| 247 | + |
| 248 | + var buf bytes.Buffer |
| 249 | + err = stitchProof.Encode(&buf) |
| 250 | + require.NoError(t, err) |
| 251 | + t.Logf("Stich proof for block %d: %x", blockHeight, buf.Bytes()) |
| 252 | + } |
| 253 | + |
| 254 | + _, err = fullFile.Verify(ctx, proof.MockVerifierCtx) |
| 255 | + require.NoError(t, err) |
| 256 | + |
| 257 | + var buf bytes.Buffer |
| 258 | + err = fullFile.Encode(&buf) |
| 259 | + require.NoError(t, err) |
| 260 | + |
| 261 | + // Write the full file to disk. |
| 262 | + err = os.MkdirAll("testdata", 0755) |
| 263 | + require.NoError(t, err) |
| 264 | + err = os.WriteFile("testdata/downloaded.proof", buf.Bytes(), 0644) |
| 265 | + require.NoError(t, err) |
| 266 | +} |
| 267 | + |
| 268 | +// marshalUniProofType marshals the universe proof type into the RPC |
| 269 | +// counterpart. Copied from the main package to avoid circular dependency. |
| 270 | +func marshalUniProofType( |
| 271 | + proofType ProofType) (unirpc.ProofType, error) { |
| 272 | + |
| 273 | + switch proofType { |
| 274 | + case ProofTypeUnspecified: |
| 275 | + return unirpc.ProofType_PROOF_TYPE_UNSPECIFIED, nil |
| 276 | + case ProofTypeIssuance: |
| 277 | + return unirpc.ProofType_PROOF_TYPE_ISSUANCE, nil |
| 278 | + case ProofTypeTransfer: |
| 279 | + return unirpc.ProofType_PROOF_TYPE_TRANSFER, nil |
| 280 | + |
| 281 | + default: |
| 282 | + return 0, fmt.Errorf("unknown universe proof type: %v", |
| 283 | + proofType) |
| 284 | + } |
| 285 | +} |
| 286 | + |
| 287 | +// marshalUniID marshals the universe ID into the RPC counterpart. Copied from |
| 288 | +// the main package to avoid circular dependency. |
| 289 | +func marshalUniID(id Identifier) (*unirpc.ID, error) { |
| 290 | + var uniID unirpc.ID |
| 291 | + |
| 292 | + if id.GroupKey != nil { |
| 293 | + uniID.Id = &unirpc.ID_GroupKey{ |
| 294 | + GroupKey: schnorr.SerializePubKey(id.GroupKey), |
| 295 | + } |
| 296 | + } else { |
| 297 | + uniID.Id = &unirpc.ID_AssetId{ |
| 298 | + AssetId: id.AssetID[:], |
| 299 | + } |
| 300 | + } |
| 301 | + |
| 302 | + proofTypeRpc, err := marshalUniProofType(id.ProofType) |
| 303 | + if err != nil { |
| 304 | + return nil, fmt.Errorf("unable to marshal proof type: %w", err) |
| 305 | + } |
| 306 | + uniID.ProofType = proofTypeRpc |
| 307 | + |
| 308 | + return &uniID, nil |
| 309 | +} |
0 commit comments