diff --git a/chain/sync.go b/chain/sync.go index 0ba28ed..c6841ef 100644 --- a/chain/sync.go +++ b/chain/sync.go @@ -242,6 +242,10 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte, if err := c.MarkSpent(inp.KeyImage, height); err != nil { return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("mark spent %s", inp.KeyImage), err) } + case types.TxInputHTLC: + if err := c.MarkSpent(inp.KeyImage, height); err != nil { + return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("mark spent %s", inp.KeyImage), err) + } case types.TxInputZC: if err := c.MarkSpent(inp.KeyImage, height); err != nil { return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("mark spent %s", inp.KeyImage), err) diff --git a/chain/sync_test.go b/chain/sync_test.go index 5f37d2d..20d3558 100644 --- a/chain/sync_test.go +++ b/chain/sync_test.go @@ -1015,3 +1015,148 @@ func TestSync_Good_ZCInputKeyImageMarkedSpent(t *testing.T) { t.Error("IsSpent(zc_key_image): got false, want true") } } + +func TestSync_Good_HTLCInputKeyImageMarkedSpent(t *testing.T) { + genesisBlob, genesisHash := makeGenesisBlockBlob() + + htlcKeyImage := types.KeyImage{0x44, 0x55, 0x66} + htlcTx := types.Transaction{ + Version: 1, + Vin: []types.TxInput{ + types.TxInputHTLC{ + HTLCOrigin: "contract-1", + Amount: 1000000000000, + KeyOffsets: []types.TxOutRef{{ + Tag: types.RefTypeGlobalIndex, + GlobalIndex: 0, + }}, + KeyImage: htlcKeyImage, + EtcDetails: wire.EncodeVarint(0), + }, + }, + Vout: []types.TxOutput{ + types.TxOutputBare{ + Amount: 900000000000, + Target: types.TxOutToKey{Key: types.PublicKey{0x21}}, + }, + }, + Extra: wire.EncodeVarint(0), + Attachment: wire.EncodeVarint(0), + } + + var txBuf bytes.Buffer + txEnc := wire.NewEncoder(&txBuf) + wire.EncodeTransaction(txEnc, &htlcTx) + htlcTxBlob := hex.EncodeToString(txBuf.Bytes()) + htlcTxHash := wire.TransactionHash(&htlcTx) + + minerTx1 := testCoinbaseTx(1) + block1 := types.Block{ + BlockHeader: types.BlockHeader{ + MajorVersion: 1, + Nonce: 42, + PrevID: genesisHash, + Timestamp: 1770897720, + }, + MinerTx: minerTx1, + TxHashes: []types.Hash{htlcTxHash}, + } + + var blk1Buf bytes.Buffer + blk1Enc := wire.NewEncoder(&blk1Buf) + wire.EncodeBlock(blk1Enc, &block1) + block1Blob := hex.EncodeToString(blk1Buf.Bytes()) + block1Hash := wire.BlockHash(&block1) + + orig := GenesisHash + GenesisHash = genesisHash.String() + t.Cleanup(func() { GenesisHash = orig }) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.URL.Path == "/getheight" { + json.NewEncoder(w).Encode(map[string]any{ + "height": 2, + "status": "OK", + }) + return + } + + var req struct { + Method string `json:"method"` + Params json.RawMessage `json:"params"` + } + json.NewDecoder(r.Body).Decode(&req) + + switch req.Method { + case "get_blocks_details": + blocks := []map[string]any{ + { + "height": uint64(0), + "timestamp": uint64(1770897600), + "base_reward": uint64(1000000000000), + "id": genesisHash.String(), + "difficulty": "1", + "type": uint64(1), + "blob": genesisBlob, + "transactions_details": []any{}, + }, + { + "height": uint64(1), + "timestamp": uint64(1770897720), + "base_reward": uint64(1000000), + "id": block1Hash.String(), + "difficulty": "100", + "type": uint64(1), + "blob": block1Blob, + "transactions_details": []map[string]any{ + { + "id": htlcTxHash.String(), + "blob": htlcTxBlob, + "fee": uint64(100000000000), + }, + }, + }, + } + + result := map[string]any{ + "blocks": blocks, + "status": "OK", + } + resultBytes, _ := json.Marshal(result) + json.NewEncoder(w).Encode(map[string]any{ + "jsonrpc": "2.0", + "id": "0", + "result": json.RawMessage(resultBytes), + }) + } + })) + defer srv.Close() + + s, _ := store.New(":memory:") + defer s.Close() + c := New(s) + + client := rpc.NewClient(srv.URL) + opts := SyncOptions{ + VerifySignatures: false, + Forks: []config.HardFork{ + {Version: config.HF1, Height: 0, Mandatory: true}, + {Version: config.HF2, Height: 0, Mandatory: true}, + }, + } + + err := c.Sync(context.Background(), client, opts) + if err != nil { + t.Fatalf("Sync: %v", err) + } + + spent, err := c.IsSpent(htlcKeyImage) + if err != nil { + t.Fatalf("IsSpent: %v", err) + } + if !spent { + t.Error("IsSpent(htlc_key_image): got false, want true") + } +} diff --git a/p2p/handshake.go b/p2p/handshake.go index c451c0a..12e42a1 100644 --- a/p2p/handshake.go +++ b/p2p/handshake.go @@ -7,6 +7,7 @@ package p2p import ( "encoding/binary" + "fmt" "dappco.re/go/core/p2p/node/levin" ) @@ -173,3 +174,20 @@ func (r *HandshakeResponse) Decode(data []byte) error { } return nil } + +// ValidateHandshakeResponse verifies that a remote peer's handshake response +// matches the expected network and satisfies the minimum build version gate. +func ValidateHandshakeResponse(resp *HandshakeResponse, expectedNetworkID [16]byte, isTestnet bool) error { + if resp.NodeData.NetworkID != expectedNetworkID { + return fmt.Errorf("p2p: peer network id %x does not match expected %x", + resp.NodeData.NetworkID, expectedNetworkID) + } + + if !MeetsMinimumBuildVersion(resp.PayloadData.ClientVersion, isTestnet) { + minBuild := MinimumRequiredBuildVersion(isTestnet) + return fmt.Errorf("p2p: peer build %q below minimum %d", + resp.PayloadData.ClientVersion, minBuild) + } + + return nil +} diff --git a/p2p/handshake_test.go b/p2p/handshake_test.go index f9e14f1..7126ce9 100644 --- a/p2p/handshake_test.go +++ b/p2p/handshake_test.go @@ -7,6 +7,7 @@ package p2p import ( "encoding/binary" + "strings" "testing" "dappco.re/go/core/blockchain/config" @@ -154,3 +155,56 @@ func TestDecodePeerlist_Good_EmptyBlob(t *testing.T) { t.Errorf("empty peerlist: got %d entries, want 0", len(entries)) } } + +func TestValidateHandshakeResponse_Good(t *testing.T) { + resp := &HandshakeResponse{ + NodeData: NodeData{ + NetworkID: config.NetworkIDTestnet, + }, + PayloadData: CoreSyncData{ + ClientVersion: "6.0.1.2[go-blockchain]", + }, + } + + if err := ValidateHandshakeResponse(resp, config.NetworkIDTestnet, true); err != nil { + t.Fatalf("ValidateHandshakeResponse: %v", err) + } +} + +func TestValidateHandshakeResponse_BadNetwork(t *testing.T) { + resp := &HandshakeResponse{ + NodeData: NodeData{ + NetworkID: config.NetworkIDMainnet, + }, + PayloadData: CoreSyncData{ + ClientVersion: "6.0.1.2[go-blockchain]", + }, + } + + err := ValidateHandshakeResponse(resp, config.NetworkIDTestnet, true) + if err == nil { + t.Fatal("ValidateHandshakeResponse: expected network mismatch error") + } + if !strings.Contains(err.Error(), "network id") { + t.Fatalf("ValidateHandshakeResponse error: got %v, want network id mismatch", err) + } +} + +func TestValidateHandshakeResponse_BadBuildVersion(t *testing.T) { + resp := &HandshakeResponse{ + NodeData: NodeData{ + NetworkID: config.NetworkIDMainnet, + }, + PayloadData: CoreSyncData{ + ClientVersion: "0.0.1.0", + }, + } + + err := ValidateHandshakeResponse(resp, config.NetworkIDMainnet, false) + if err == nil { + t.Fatal("ValidateHandshakeResponse: expected build version error") + } + if !strings.Contains(err.Error(), "below minimum") { + t.Fatalf("ValidateHandshakeResponse error: got %v, want build minimum failure", err) + } +} diff --git a/sync_loop.go b/sync_loop.go index 524a68c..6ff1f77 100644 --- a/sync_loop.go +++ b/sync_loop.go @@ -102,13 +102,8 @@ func runChainSyncOnce(ctx context.Context, blockchain *chain.Chain, chainConfig return coreerr.E("runChainSyncOnce", "decode handshake", err) } - if !p2p.MeetsMinimumBuildVersion(handshakeResp.PayloadData.ClientVersion, chainConfig.IsTestnet) { - minBuild := p2p.MinimumRequiredBuildVersion(chainConfig.IsTestnet) - return coreerr.E( - "runChainSyncOnce", - fmt.Sprintf("peer build %q below minimum %d", handshakeResp.PayloadData.ClientVersion, minBuild), - nil, - ) + if err := p2p.ValidateHandshakeResponse(&handshakeResp, chainConfig.NetworkID, chainConfig.IsTestnet); err != nil { + return coreerr.E("runChainSyncOnce", "validate handshake", err) } localSync := p2p.CoreSyncData{