fix(sync): validate peers and persist HTLC spends
Centralise handshake response validation so outbound sync checks both network identity and minimum peer build version through the p2p layer. Also record HTLC key images as spent during block processing, matching the HF1 input semantics and preventing those spends from being omitted from chain state. Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
0ba5bbe49c
commit
21c5d49ef9
5 changed files with 223 additions and 7 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue