fix(blockchain): enforce HF5 freeze and peer build gate
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
8e6dc326df
commit
f7ee451fc4
5 changed files with 238 additions and 2 deletions
|
|
@ -217,8 +217,8 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
|
||||||
|
|
||||||
txHash := wire.TransactionHash(&tx)
|
txHash := wire.TransactionHash(&tx)
|
||||||
|
|
||||||
// Validate transaction semantics.
|
// Validate transaction semantics, including the HF5 freeze window.
|
||||||
if err := consensus.ValidateTransaction(&tx, txBlobData, opts.Forks, height); err != nil {
|
if err := consensus.ValidateTransactionInBlock(&tx, txBlobData, opts.Forks, height); err != nil {
|
||||||
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("validate tx %s", txHash), err)
|
return coreerr.E("Chain.processBlockBlobs", fmt.Sprintf("validate tx %s", txHash), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
@ -735,6 +736,83 @@ func TestSync_Bad_InvalidBlockBlob(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSync_Bad_PreHardforkFreeze(t *testing.T) {
|
||||||
|
genesisBlob, genesisHash := makeGenesisBlockBlob()
|
||||||
|
|
||||||
|
regularTx := types.Transaction{
|
||||||
|
Version: 1,
|
||||||
|
Vin: []types.TxInput{
|
||||||
|
types.TxInputToKey{
|
||||||
|
Amount: 1000000000000,
|
||||||
|
KeyOffsets: []types.TxOutRef{{
|
||||||
|
Tag: types.RefTypeGlobalIndex,
|
||||||
|
GlobalIndex: 0,
|
||||||
|
}},
|
||||||
|
KeyImage: types.KeyImage{0xaa, 0xbb, 0xcc},
|
||||||
|
EtcDetails: wire.EncodeVarint(0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Vout: []types.TxOutput{
|
||||||
|
types.TxOutputBare{
|
||||||
|
Amount: 900000000000,
|
||||||
|
Target: types.TxOutToKey{Key: types.PublicKey{0x02}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extra: wire.EncodeVarint(0),
|
||||||
|
Attachment: wire.EncodeVarint(0),
|
||||||
|
}
|
||||||
|
|
||||||
|
var txBuf bytes.Buffer
|
||||||
|
txEnc := wire.NewEncoder(&txBuf)
|
||||||
|
wire.EncodeTransaction(txEnc, ®ularTx)
|
||||||
|
regularTxBlob := txBuf.Bytes()
|
||||||
|
regularTxHash := wire.TransactionHash(®ularTx)
|
||||||
|
|
||||||
|
minerTx1 := testCoinbaseTx(1)
|
||||||
|
block1 := types.Block{
|
||||||
|
BlockHeader: types.BlockHeader{
|
||||||
|
MajorVersion: 1,
|
||||||
|
Nonce: 42,
|
||||||
|
PrevID: genesisHash,
|
||||||
|
Timestamp: 1770897720,
|
||||||
|
},
|
||||||
|
MinerTx: minerTx1,
|
||||||
|
TxHashes: []types.Hash{regularTxHash},
|
||||||
|
}
|
||||||
|
|
||||||
|
var blk1Buf bytes.Buffer
|
||||||
|
blk1Enc := wire.NewEncoder(&blk1Buf)
|
||||||
|
wire.EncodeBlock(blk1Enc, &block1)
|
||||||
|
block1Blob := blk1Buf.Bytes()
|
||||||
|
|
||||||
|
orig := GenesisHash
|
||||||
|
GenesisHash = genesisHash.String()
|
||||||
|
t.Cleanup(func() { GenesisHash = orig })
|
||||||
|
|
||||||
|
s, _ := store.New(":memory:")
|
||||||
|
defer s.Close()
|
||||||
|
c := New(s)
|
||||||
|
|
||||||
|
opts := SyncOptions{
|
||||||
|
Forks: []config.HardFork{
|
||||||
|
{Version: config.HF0Initial, Height: 0, Mandatory: true},
|
||||||
|
{Version: config.HF5, Height: 2, Mandatory: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.processBlockBlobs(genesisBlob, nil, 0, 1, opts); err != nil {
|
||||||
|
t.Fatalf("process genesis: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.processBlockBlobs(block1Blob, [][]byte{regularTxBlob}, 1, 100, opts)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected freeze rejection, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "freeze") {
|
||||||
|
t.Fatalf("expected freeze error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// testCoinbaseTxV2 creates a v2 (post-HF4) coinbase transaction with Zarcanum outputs.
|
// testCoinbaseTxV2 creates a v2 (post-HF4) coinbase transaction with Zarcanum outputs.
|
||||||
func testCoinbaseTxV2(height uint64) types.Transaction {
|
func testCoinbaseTxV2(height uint64) types.Transaction {
|
||||||
return types.Transaction{
|
return types.Transaction{
|
||||||
|
|
|
||||||
78
p2p/version.go
Normal file
78
p2p/version.go
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
|
||||||
|
//
|
||||||
|
// Licensed under the European Union Public Licence (EUPL) version 1.2.
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
package p2p
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MinimumRequiredBuildVersionMainnet matches the C++ daemon's mainnet gate.
|
||||||
|
MinimumRequiredBuildVersionMainnet uint64 = 601
|
||||||
|
|
||||||
|
// MinimumRequiredBuildVersionTestnet matches the C++ daemon's testnet gate.
|
||||||
|
MinimumRequiredBuildVersionTestnet uint64 = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// MinimumRequiredBuildVersion returns the minimum accepted peer version gate
|
||||||
|
// for the given network.
|
||||||
|
func MinimumRequiredBuildVersion(isTestnet bool) uint64 {
|
||||||
|
if isTestnet {
|
||||||
|
return MinimumRequiredBuildVersionTestnet
|
||||||
|
}
|
||||||
|
return MinimumRequiredBuildVersionMainnet
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeerBuildVersion extracts the numeric major.minor.revision component from a
|
||||||
|
// daemon client version string.
|
||||||
|
//
|
||||||
|
// The daemon formats its version as "major.minor.revision.build[extra]".
|
||||||
|
// The minimum build gate compares the first three components, so
|
||||||
|
// "6.0.1.2[go-blockchain]" becomes 601.
|
||||||
|
func PeerBuildVersion(clientVersion string) (uint64, bool) {
|
||||||
|
parts := strings.SplitN(clientVersion, ".", 4)
|
||||||
|
if len(parts) < 3 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
major, err := strconv.ParseUint(parts[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
minor, err := strconv.ParseUint(parts[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
revPart := parts[2]
|
||||||
|
for i := 0; i < len(revPart); i++ {
|
||||||
|
if revPart[i] < '0' || revPart[i] > '9' {
|
||||||
|
revPart = revPart[:i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if revPart == "" {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
revision, err := strconv.ParseUint(revPart, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return major*100 + minor*10 + revision, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MeetsMinimumBuildVersion reports whether the peer's version is acceptable
|
||||||
|
// for the given network.
|
||||||
|
func MeetsMinimumBuildVersion(clientVersion string, isTestnet bool) bool {
|
||||||
|
buildVersion, ok := PeerBuildVersion(clientVersion)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return buildVersion >= MinimumRequiredBuildVersion(isTestnet)
|
||||||
|
}
|
||||||
71
p2p/version_test.go
Normal file
71
p2p/version_test.go
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
|
||||||
|
//
|
||||||
|
// Licensed under the European Union Public Licence (EUPL) version 1.2.
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
package p2p
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestPeerBuildVersion_Good(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
want uint64
|
||||||
|
wantOK bool
|
||||||
|
}{
|
||||||
|
{"release", "6.0.1.2[go-blockchain]", 601, true},
|
||||||
|
{"two_digits", "12.3.4.5", 1234, true},
|
||||||
|
{"suffix", "6.0.1-beta.2", 601, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, ok := PeerBuildVersion(tt.input)
|
||||||
|
if ok != tt.wantOK {
|
||||||
|
t.Fatalf("PeerBuildVersion(%q) ok = %v, want %v", tt.input, ok, tt.wantOK)
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("PeerBuildVersion(%q) = %d, want %d", tt.input, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPeerBuildVersion_Bad(t *testing.T) {
|
||||||
|
tests := []string{
|
||||||
|
"",
|
||||||
|
"6",
|
||||||
|
"6.0",
|
||||||
|
"abc.def.ghi",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, input := range tests {
|
||||||
|
t.Run(input, func(t *testing.T) {
|
||||||
|
if got, ok := PeerBuildVersion(input); ok || got != 0 {
|
||||||
|
t.Fatalf("PeerBuildVersion(%q) = (%d, %v), want (0, false)", input, got, ok)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMeetsMinimumBuildVersion_Good(t *testing.T) {
|
||||||
|
if !MeetsMinimumBuildVersion("6.0.1.2[go-blockchain]", false) {
|
||||||
|
t.Fatal("expected mainnet build version to satisfy minimum")
|
||||||
|
}
|
||||||
|
if !MeetsMinimumBuildVersion("6.0.1.2[go-blockchain]", true) {
|
||||||
|
t.Fatal("expected testnet build version to satisfy minimum")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMeetsMinimumBuildVersion_Bad(t *testing.T) {
|
||||||
|
if MeetsMinimumBuildVersion("0.0.1.0", false) {
|
||||||
|
t.Fatal("expected low mainnet build version to fail")
|
||||||
|
}
|
||||||
|
if MeetsMinimumBuildVersion("0.0.1.0", true) {
|
||||||
|
t.Fatal("expected low testnet build version to fail")
|
||||||
|
}
|
||||||
|
if MeetsMinimumBuildVersion("bogus", false) {
|
||||||
|
t.Fatal("expected malformed version to fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -102,6 +102,15 @@ func runChainSyncOnce(ctx context.Context, blockchain *chain.Chain, chainConfig
|
||||||
return coreerr.E("runChainSyncOnce", "decode handshake", err)
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
localSync := p2p.CoreSyncData{
|
localSync := p2p.CoreSyncData{
|
||||||
CurrentHeight: localHeight,
|
CurrentHeight: localHeight,
|
||||||
ClientVersion: config.ClientVersion,
|
ClientVersion: config.ClientVersion,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue